├── tests ├── keys │ ├── data │ ├── ecdh.bin │ ├── ecdsa.sig │ ├── hmac.sig │ ├── aes_cbc.bin │ ├── aes_ctr.bin │ ├── aes_gcm.bin │ ├── rsa-oaep.bin │ ├── rsa-pkcs1.sig │ ├── aes_cbc.json │ ├── aes_ctr.json │ ├── aes_gcm.json │ ├── ecdsa.pub.json │ ├── hmac.json │ ├── ecdsa.json │ ├── ecdh.pub.json │ ├── ecdh.json │ ├── rsa-pkcs1.pub.json │ ├── rsa-oaep.pub.json │ ├── rsa-pkcs1.json │ └── rsa-oaep.json ├── spelling.R ├── testthat.R ├── testthat │ ├── test_hmac.R │ ├── test_header.R │ ├── test_claims.R │ ├── test_aes.R │ ├── test_rsa.R │ ├── test_ec.R │ ├── test_exp.R │ ├── test_sizes.R │ └── test_examples.R └── js │ ├── hmac.js │ ├── aes.js │ ├── rsa.js │ └── ec.js ├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── .gitignore ├── LICENSE ├── .Rbuildignore ├── inst └── WORDLIST ├── jose.Rproj ├── NEWS ├── man ├── base64url_encode.Rd ├── jwk.Rd ├── jwt_claim.Rd └── jwt_encode.Rd ├── NAMESPACE ├── appveyor.yml ├── DESCRIPTION ├── R ├── base64url.R ├── write.R ├── read.R ├── claim.R └── jwt.R ├── vignettes ├── jwk.Rmd └── jwt.Rmd └── readme.md /tests/keys/data: -------------------------------------------------------------------------------- 1 | testje -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | .Rproj.user 4 | .Rhistory 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Jeroen Ooms 3 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | spelling::spell_check_test(vignettes = TRUE, error = FALSE) 2 | -------------------------------------------------------------------------------- /tests/keys/ecdh.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/ecdh.bin -------------------------------------------------------------------------------- /tests/keys/ecdsa.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/ecdsa.sig -------------------------------------------------------------------------------- /tests/keys/hmac.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/hmac.sig -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(jose) 3 | 4 | test_check("jose") 5 | -------------------------------------------------------------------------------- /tests/keys/aes_cbc.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/aes_cbc.bin -------------------------------------------------------------------------------- /tests/keys/aes_ctr.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/aes_ctr.bin -------------------------------------------------------------------------------- /tests/keys/aes_gcm.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/aes_gcm.bin -------------------------------------------------------------------------------- /tests/keys/rsa-oaep.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/rsa-oaep.bin -------------------------------------------------------------------------------- /tests/keys/rsa-pkcs1.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/jose/HEAD/tests/keys/rsa-pkcs1.sig -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^readme.md$ 5 | ^appveyor\.yml$ 6 | ^\.github$ 7 | -------------------------------------------------------------------------------- /tests/keys/aes_cbc.json: -------------------------------------------------------------------------------- 1 | {"alg":"A256CBC","ext":true,"k":"KPLSwvItzgdpImJd4-vfy0-uAqmeGZ0A4HOvPoPD2zY","key_ops":["encrypt","decrypt"],"kty":"oct"} 2 | -------------------------------------------------------------------------------- /tests/keys/aes_ctr.json: -------------------------------------------------------------------------------- 1 | {"alg":"A256CTR","ext":true,"k":"QntzWuHR0jn9hzxMamsjI8HWJUGbldPTCgH0vdu8vco","key_ops":["encrypt","decrypt"],"kty":"oct"} 2 | -------------------------------------------------------------------------------- /tests/keys/aes_gcm.json: -------------------------------------------------------------------------------- 1 | {"alg":"A256GCM","ext":true,"k":"6jX7PTEQAEDE4CEJcZt9Fp8mh5_c98gboXnjfA-0BPo","key_ops":["encrypt","decrypt"],"kty":"oct"} 2 | -------------------------------------------------------------------------------- /tests/keys/ecdsa.pub.json: -------------------------------------------------------------------------------- 1 | {"crv":"P-256","ext":true,"key_ops":["verify"],"kty":"EC","x":"cy16Sd3QYdzFz8IDxqZmN5l1DVkevDAaYTD5Jko85Dg","y":"SDs8Agj66BNtAmZkFulcrizvcK7IO6CzvgWmOQj5Uxo"} 2 | -------------------------------------------------------------------------------- /tests/keys/hmac.json: -------------------------------------------------------------------------------- 1 | {"alg":"HS256","ext":true,"k":"qzM39I7vFUQBjnqyxSOe88ZPEHTVEagw4CLO9DiX0ye4Brlu33uFvwScxyDvhfeIOrCT7LyuhJJMftn6f_kRKw","key_ops":["sign","verify"],"kty":"oct"} 2 | -------------------------------------------------------------------------------- /tests/keys/ecdsa.json: -------------------------------------------------------------------------------- 1 | {"crv":"P-256","d":"gi-2wo0WURZkzCKzkWo3TDINGl3VBVXwMtz9Kj0hMQQ","ext":true,"key_ops":["sign"],"kty":"EC","x":"cy16Sd3QYdzFz8IDxqZmN5l1DVkevDAaYTD5Jko85Dg","y":"SDs8Agj66BNtAmZkFulcrizvcK7IO6CzvgWmOQj5Uxo"} 2 | -------------------------------------------------------------------------------- /tests/keys/ecdh.pub.json: -------------------------------------------------------------------------------- 1 | {"crv":"P-521","ext":true,"key_ops":[],"kty":"EC","x":"ASsgMYsBS1EHm1W1Xul2O6TRg9YX2SD1gkoHoDkHIcFAq4v5BMJ0MR4g6SKttblcn1Yv2HKi1uGBvzuWJzdozipp","y":"Abv0rPEJGBCO4F98VuMI4921dvXv6Sq9cF8NcRQt0-bOzeER-YQS8XMSKXyJG2F_J80XP6YuBesPHyzvqYE-J4wL"} 2 | -------------------------------------------------------------------------------- /tests/testthat/test_hmac.R: -------------------------------------------------------------------------------- 1 | context("HMAC signature") 2 | 3 | test_that("ECDSA works", { 4 | key <- read_jwk("../keys/hmac.json") 5 | sig <- readBin("../keys/hmac.sig", raw(), 100) 6 | expect_identical(sig, unclass(openssl::sha256(charToRaw("testje"), key = key))) 7 | }) 8 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | AES 2 | AppVeyor 3 | ECDSA 4 | Github 5 | HMAC 6 | IETF 7 | JSON 8 | JWA 9 | JWE 10 | JWK 11 | JWS 12 | JWT 13 | LetsEncrypt 14 | NumericDate 15 | OAuth 16 | RSA 17 | RStudio 18 | StringOrURI 19 | WebCryptoAPI 20 | bitsize 21 | https 22 | io 23 | json 24 | jwt 25 | natively 26 | nd 27 | openssl 28 | pubkey 29 | rfc 30 | sha 31 | workgroup 32 | -------------------------------------------------------------------------------- /tests/keys/ecdh.json: -------------------------------------------------------------------------------- 1 | {"crv":"P-521","d":"AJxVP8b-r-cpHiXEPVw-QM50W9CKYbhAH4ejOQLXD1IpKC7wsxg0iVB2wQyGV8yn9u14Ed1rY6jY_FQjhbRzz6_n","ext":true,"key_ops":["deriveKey","deriveBits"],"kty":"EC","x":"ASsgMYsBS1EHm1W1Xul2O6TRg9YX2SD1gkoHoDkHIcFAq4v5BMJ0MR4g6SKttblcn1Yv2HKi1uGBvzuWJzdozipp","y":"Abv0rPEJGBCO4F98VuMI4921dvXv6Sq9cF8NcRQt0-bOzeER-YQS8XMSKXyJG2F_J80XP6YuBesPHyzvqYE-J4wL"} 2 | -------------------------------------------------------------------------------- /jose.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,namespace 22 | -------------------------------------------------------------------------------- /tests/keys/rsa-pkcs1.pub.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "RS256", 3 | "e": "AQAB", 4 | "ext": true, 5 | "key_ops": [ 6 | "verify" 7 | ], 8 | "kty": "RSA", 9 | "n": "rG2s5sqC_dciWLZMxFrXf-Wy5g8QzxeDa7266zLOujOIydwlsoegXCaTRk7A1nm3qpxkYdpUIO2uAmR4QIIl3E7fLZ6o9HKIJk64JqSOtibG3i1TRtiyS703CKK7rTpvvIx4uBKbl4PITC8u_EdaJMyF4q_kkGTFG7qwQNaHgqwb02A46W3ZbUQbyzLKZe3TpViMm0G8yz5Z2EOY9ijK2nByfaMa0dB53t84KFduYrxqGvuC8BVTUOJ67j-9A9kd2vkgPlyqz5beZP12pUtpDnkw2V9Q_4_fcnHp_qeItpeGTbwcGmeWTdWGGXM0WdyDNjqapEzkPs1vB894Glqsxw" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /tests/keys/rsa-oaep.pub.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "RSA-OAEP-256", 3 | "e": "AQAB", 4 | "ext": true, 5 | "key_ops": [ 6 | "encrypt" 7 | ], 8 | "kty": "RSA", 9 | "n": "xqL9-CoruIuemD1aF8Z4MC0IQkvnxtiRn67CkQPo967tHHIgd1dYYS6z3aIaNYFMIK6EclYaQYbuO3-JUco2hvvF4OYQ5SH-yP87EmcsZaUzOjGSR9A62cw3LJzyfTUnpaCqWI8aE6SrQrfzPq-y-tNnynihvEOLHTSAyUp7Vi3xxAnQ_YeydqTDFge2cfUJGLXHKve_uF9coTUNYWjMs8szcWnTg1m3LMaclFM7tfovNiNtkP1hWASaUOk-YTISQujt-eFroXyN-F6B3xyVcQ-7z-7s-KznsY4MxybCsioCAPwpkU_UoByxcrpBepyfhhAU43XmfuWilEc8n-A3Gw" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 1.2 2 | - jwt_encode_sig() now allows to override the typ field via headers #15 3 | - jwt_decode functions now check the 'exp' and 'nbf' fields and raise 4 | and error if token has expired. 5 | 6 | 1.1 7 | - Allow for empty list attributes in jwt_claim(), issue #13 8 | 9 | 1.0 10 | - jwt_encode_sig() and jwt_decode_sig() now use proper 64bit signatures 11 | as described in the spec (instead of openssl DER structures) 12 | 13 | 0.2 14 | - jwt_encode_hmac() and jwt_encode_sig() gain a 'header' parameter 15 | - Add spell checker, update maintainer email address 16 | -------------------------------------------------------------------------------- /man/base64url_encode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/base64url.R 3 | \name{base64url_encode} 4 | \alias{base64url_encode} 5 | \alias{base64url_decode} 6 | \title{Base64URL encoding} 7 | \usage{ 8 | base64url_encode(bin) 9 | 10 | base64url_decode(text) 11 | } 12 | \arguments{ 13 | \item{bin}{a binary blob to encode} 14 | 15 | \item{text}{a base64url encoded string} 16 | } 17 | \description{ 18 | The \code{base64url_encode} functions are a variant of the standard base64. They are 19 | specified in Section 5 of RFC 4648 as a URL-safe alternative. They use different symbols 20 | for the 62:nd and 63:rd alphabet character and do not include trailing \code{==} 21 | padding. 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/test_header.R: -------------------------------------------------------------------------------- 1 | context("Generating JWT headers") 2 | 3 | test_that("Headers work for hmac", { 4 | key <- charToRaw("SuperSecret") 5 | jwt <- jwt_encode_hmac(jwt_claim(test = "test"), secret = key, header = list(test = "test")) 6 | strings <- strsplit(jwt, ".", fixed = TRUE)[[1]] 7 | expect_true(fromJSON(rawToChar(base64url_decode(strings[1])))$test == "test") 8 | }) 9 | 10 | test_that("Headers work for sig", { 11 | mykey <- openssl::rsa_keygen() 12 | pubkey <- mykey$pubkey 13 | jwt <- jwt_encode_sig(jwt_claim(test = "test"), mykey, header = list(test = "test")) 14 | strings <- strsplit(jwt, ".", fixed = TRUE)[[1]] 15 | expect_true(fromJSON(rawToChar(base64url_decode(strings[1])))$test == "test") 16 | }) 17 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,jwt_claim) 4 | export(base64url_decode) 5 | export(base64url_encode) 6 | export(jwk_read) 7 | export(jwk_write) 8 | export(jwt_claim) 9 | export(jwt_decode_hmac) 10 | export(jwt_decode_sig) 11 | export(jwt_encode_hmac) 12 | export(jwt_encode_sig) 13 | export(jwt_split) 14 | export(read_jwk) 15 | export(write_jwk) 16 | importFrom(jsonlite,fromJSON) 17 | importFrom(jsonlite,toJSON) 18 | importFrom(jsonlite,validate) 19 | importFrom(openssl,base64_decode) 20 | importFrom(openssl,base64_encode) 21 | importFrom(openssl,bignum) 22 | importFrom(openssl,read_key) 23 | importFrom(openssl,read_pubkey) 24 | importFrom(openssl,sha2) 25 | importFrom(openssl,signature_create) 26 | importFrom(openssl,signature_verify) 27 | -------------------------------------------------------------------------------- /tests/testthat/test_claims.R: -------------------------------------------------------------------------------- 1 | context("Generating JWT claims") 2 | 3 | test_that("StringOrURI", { 4 | expect_is(jwt_claim(iss = "foo")$iss, "character") 5 | expect_is(jwt_claim(sub = "foo")$sub, "character") 6 | expect_is(jwt_claim(aud = c("foo", "bar"))$aud, "character") 7 | expect_is(jwt_claim(iss = "http://www.google.com")$iss, "character") 8 | expect_error(jwt_claim(iss = 123), "Invalid") 9 | expect_error(jwt_claim(iss = "bla:bla"), "Invalid") 10 | }) 11 | 12 | test_that("NumericDate", { 13 | val <- unclass(Sys.time()) 14 | expect_is(jwt_claim(exp = val)$exp, "numeric") 15 | expect_is(jwt_claim(nbf = val)$nbf, "numeric") 16 | expect_is(jwt_claim(iat = val)$iat, "numeric") 17 | expect_error(jwt_claim(exp = "foo"), "Invalid") 18 | expect_error(jwt_claim(exp = 1e10), "Invalid") 19 | }) 20 | -------------------------------------------------------------------------------- /tests/js/hmac.js: -------------------------------------------------------------------------------- 1 | //See: https://github.com/diafygi/webcrypto-examples 2 | function str2buf(str) { 3 | var bufView = new Uint8Array(str.length); 4 | for (var i=0, strLen=str.length; i. These standards provide 10 | modern signing and encryption formats that are natively supported by browsers 11 | via the JavaScript WebCryptoAPI , 12 | and used by services like OAuth 2.0, LetsEncrypt, and Github Apps. 13 | License: MIT + file LICENSE 14 | URL: https://r-lib.r-universe.dev/jose 15 | BugReports: https://github.com/r-lib/jose/issues 16 | Depends: openssl (>= 1.2.1) 17 | Imports: jsonlite 18 | RoxygenNote: 7.1.2 19 | VignetteBuilder: knitr 20 | Suggests: 21 | spelling, 22 | testthat, 23 | knitr, 24 | rmarkdown 25 | Encoding: UTF-8 26 | Language: en-US 27 | -------------------------------------------------------------------------------- /R/base64url.R: -------------------------------------------------------------------------------- 1 | #' Base64URL encoding 2 | #' 3 | #' The \code{base64url_encode} functions are a variant of the standard base64. They are 4 | #' specified in Section 5 of RFC 4648 as a URL-safe alternative. They use different symbols 5 | #' for the 62:nd and 63:rd alphabet character and do not include trailing \code{==} 6 | #' padding. 7 | #' 8 | #' @rdname base64url_encode 9 | #' @importFrom openssl base64_encode base64_decode 10 | #' @param text a base64url encoded string 11 | #' @param bin a binary blob to encode 12 | #' @export 13 | base64url_encode <- function(bin){ 14 | text <- base64_encode(bin) 15 | sub("=+$", "", chartr('+/', '-_', text)) 16 | } 17 | 18 | #' @rdname base64url_encode 19 | #' @importFrom openssl base64_decode 20 | #' @export 21 | base64url_decode <- function(text){ 22 | text <- fix_padding(chartr('-_', '+/', text)) 23 | base64_decode(text) 24 | } 25 | 26 | # Ensures base64 length is a multiple of 4 27 | fix_padding <- function(text){ 28 | text <- gsub("[\r\n]", "", text)[[1]] 29 | mod <- nchar(text) %% 4; 30 | if(mod > 0){ 31 | padding <- paste(rep("=", (4 - mod)), collapse = "") 32 | text <- paste0(text, padding) 33 | } 34 | text 35 | } 36 | -------------------------------------------------------------------------------- /tests/testthat/test_rsa.R: -------------------------------------------------------------------------------- 1 | context("RSA signatures and encryption") 2 | 3 | test_that("RSA PKCS1 signatures", { 4 | key <- read_jwk("../keys/rsa-pkcs1.json") 5 | pubkey <- read_jwk("../keys/rsa-pkcs1.pub.json") 6 | data <- readBin("../keys/data", raw(), 1e4) 7 | sig <- readBin("../keys/rsa-pkcs1.sig", raw(), 1e4) 8 | expect_is(key, "key") 9 | expect_is(pubkey, "pubkey") 10 | expect_is(key, "rsa") 11 | expect_is(pubkey, "rsa") 12 | expect_identical(pubkey, as.list(key)$pubkey) 13 | expect_true(openssl::signature_verify(data, sig, openssl::sha256, pubkey)) 14 | expect_true(openssl::signature_verify("../keys/data", "../keys/rsa-pkcs1.sig", openssl::sha256, pubkey)) 15 | }) 16 | 17 | test_that("RSA OAEP encryption", { 18 | key <- read_jwk("../keys/rsa-oaep.json") 19 | pubkey <- read_jwk("../keys/rsa-oaep.pub.json") 20 | data <- readBin("../keys/data", raw(), 1e4) 21 | bin <- readBin("../keys/rsa-oaep.bin", raw(), 1e4) 22 | expect_is(key, "key") 23 | expect_is(pubkey, "pubkey") 24 | expect_is(key, "rsa") 25 | expect_is(pubkey, "rsa") 26 | 27 | ## Does not work, rsa_decrypt does not use OAEP I think 28 | #expect_identical(data, openssl::rsa_decrypt(bin, key)) 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /tests/testthat/test_ec.R: -------------------------------------------------------------------------------- 1 | context("EC signatures and diffie hellman") 2 | 3 | test_that("ECDSA works", { 4 | key <- read_jwk("../keys/ecdsa.json") 5 | pubkey <- read_jwk("../keys/ecdsa.pub.json") 6 | sig <- readBin("../keys/ecdsa.sig", raw(), 1e4) 7 | expect_is(key, "key") 8 | expect_is(pubkey, "pubkey") 9 | expect_is(key, "ecdsa") 10 | expect_is(pubkey, "ecdsa") 11 | expect_identical(pubkey, as.list(key)$pubkey) 12 | 13 | # Does not work yet because webcrypto does not use DER format for binary data: 14 | # https://chromium.googlesource.com/chromium/src/+/master/components/webcrypto/algorithms/ecdsa.cc#63 15 | # expect_true(openssl::signature_verify(charToRaw("testje"), sig, openssl::sha256, pubkey)) 16 | # expect_true(openssl::signature_verify("../keys/data", "../keys/ecdsa.sig", openssl::sha256, pubkey)) 17 | }) 18 | 19 | test_that("ECDH works", { 20 | key <- read_jwk("../keys/ecdh.json") 21 | pubkey <- read_jwk("../keys/ecdh.pub.json") 22 | bin <- readBin("../keys/ecdh.bin", raw(), 1e4) 23 | expect_is(key, "key") 24 | expect_is(pubkey, "pubkey") 25 | expect_is(key, "ecdsa") 26 | expect_is(pubkey, "ecdsa") 27 | expect_identical(pubkey, as.list(key)$pubkey) 28 | expect_equal(openssl::ec_dh(key, pubkey), bin) 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /tests/testthat/test_exp.R: -------------------------------------------------------------------------------- 1 | context("Test token expiration") 2 | 3 | test_that("Headers work for hmac", { 4 | secret <- charToRaw("SuperSecret") 5 | privkey <- openssl::rsa_keygen() 6 | pubkey <- privkey$pubkey 7 | claim1 <- jwt_claim("test", exp = Sys.time()) 8 | claim2 <- jwt_claim("test", exp = Sys.time() - 100) 9 | claim3 <- jwt_claim("test", nbf = Sys.time()) 10 | claim4 <- jwt_claim("test", nbf = Sys.time() + 100) 11 | jwth1 <- jwt_encode_hmac(claim1, secret = secret) 12 | jwth2 <- jwt_encode_hmac(claim2, secret = secret) 13 | jwth3 <- jwt_encode_hmac(claim3, secret = secret) 14 | jwth4 <- jwt_encode_hmac(claim4, secret = secret) 15 | jwtr1 <- jwt_encode_sig(claim1, privkey) 16 | jwtr2 <- jwt_encode_sig(claim2, privkey) 17 | jwtr3 <- jwt_encode_sig(claim3, privkey) 18 | jwtr4 <- jwt_encode_sig(claim4, privkey) 19 | expect_equal(jwt_decode_hmac(jwth1, secret)$iss, "test") 20 | expect_error(jwt_decode_hmac(jwth2, secret), "expired") 21 | expect_equal(jwt_decode_hmac(jwth3, secret)$iss, "test") 22 | expect_error(jwt_decode_hmac(jwth4, secret), "before") 23 | expect_equal(jwt_decode_sig(jwtr1, pubkey)$iss, "test") 24 | expect_error(jwt_decode_sig(jwtr2, pubkey), "expired") 25 | expect_equal(jwt_decode_sig(jwtr3, pubkey)$iss, "test") 26 | expect_error(jwt_decode_sig(jwtr4, pubkey), "before") 27 | }) 28 | -------------------------------------------------------------------------------- /man/jwt_claim.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/claim.R 3 | \name{jwt_claim} 4 | \alias{jwt_claim} 5 | \title{Generate claim} 6 | \usage{ 7 | jwt_claim( 8 | iss = NULL, 9 | sub = NULL, 10 | aud = NULL, 11 | exp = NULL, 12 | nbf = NULL, 13 | iat = Sys.time(), 14 | jti = NULL, 15 | ... 16 | ) 17 | } 18 | \arguments{ 19 | \item{iss}{(Issuer) Claim, should be rfc7519 'StringOrURI' value} 20 | 21 | \item{sub}{(Subject) Claim, should be rfc7519 'StringOrURI' value} 22 | 23 | \item{aud}{(Audience) Claim, should contain one or rfc7519 'StringOrURI' values} 24 | 25 | \item{exp}{(Expiration Time) Claim, should be rfc7519 'NumericDate' value; R 26 | \code{POSIXct} values are automatically coerced.} 27 | 28 | \item{nbf}{(Not Before) Claim, should be rfc7519 'NumericDate' value; R 29 | \code{POSIXct} values are automatically coerced.} 30 | 31 | \item{iat}{(Issued At) Claim, should be rfc7519 'NumericDate' value; R 32 | \code{POSIXct} values are automatically coerced.} 33 | 34 | \item{jti}{(JWT ID) Claim, optional unique identifier for the JWT} 35 | 36 | \item{...}{additional custom claims to include} 37 | } 38 | \description{ 39 | Helper function to create a named list used as the claim of a JWT payload. 40 | See \url{https://datatracker.ietf.org/doc/html/rfc7519#section-4.1} for details. 41 | } 42 | -------------------------------------------------------------------------------- /vignettes/jwk.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reading/Writing JSON Web Keys (JWK) in R" 3 | date: "`r Sys.Date()`" 4 | output: 5 | html_document 6 | vignette: > 7 | %\VignetteIndexEntry{Reading/Writing JSON Web Keys (JWK) in R} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include=FALSE} 13 | knitr::opts_chunk$set(echo = TRUE) 14 | knitr::opts_chunk$set(comment = "") 15 | ``` 16 | 17 | 18 | ### RSA / ECDSA keys 19 | 20 | JSON Web Keys (JWK) is a format specified in [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517) for storing RSA/EC/AES keys in a JSON based format. It can be used to import/export such keys in the browser using the new [W3C WebCryptoAPI](https://www.w3.org/TR/WebCryptoAPI/). 21 | 22 | The `jose` package makes it easy to read/write such keys in R for use with JWT or any other functionality from the `openssl` package. 23 | 24 | 25 | ```{r} 26 | library(openssl) 27 | library(jose) 28 | 29 | # Generate a ECDSA key 30 | key <- openssl::ec_keygen() 31 | jsonlite::prettify(write_jwk(key)) 32 | 33 | # Use public key 34 | pubkey <- as.list(key)$pubkey 35 | json <- write_jwk(pubkey) 36 | jsonlite::prettify(json) 37 | 38 | # Read JWK key 39 | (out <- read_jwk(json)) 40 | identical(pubkey, out) 41 | ``` 42 | 43 | 44 | ### AES/HMAC keys 45 | 46 | JWT also specifies a format for encoding AES/HMAC secrets. Such secret keys are simply raw bytes. 47 | 48 | ```{r} 49 | # Random secret 50 | (key <- rand_bytes(16)) 51 | (jwk <- write_jwk(key)) 52 | read_jwk(jwk) 53 | ``` 54 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | pull_request: 6 | 7 | name: R-CMD-check.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-13, r: 'release'} 22 | - {os: macos-14, r: 'release'} 23 | - {os: windows-latest, r: '4.1'} 24 | - {os: windows-latest, r: '4.2'} 25 | - {os: windows-latest, r: 'release'} 26 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 27 | - {os: ubuntu-latest, r: 'release'} 28 | - {os: ubuntu-latest, r: 'oldrel-1'} 29 | 30 | env: 31 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 32 | R_KEEP_PKG_SOURCE: yes 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - uses: r-lib/actions/setup-pandoc@v2 38 | 39 | - uses: r-lib/actions/setup-r@v2 40 | with: 41 | r-version: ${{ matrix.config.r }} 42 | http-user-agent: ${{ matrix.config.http-user-agent }} 43 | use-public-rspm: true 44 | 45 | - uses: r-lib/actions/setup-r-dependencies@v2 46 | with: 47 | extra-packages: any::rcmdcheck 48 | needs: check 49 | 50 | - uses: r-lib/actions/check-r-package@v2 51 | -------------------------------------------------------------------------------- /R/write.R: -------------------------------------------------------------------------------- 1 | #' JSON web-keys 2 | #' 3 | #' Read and write RSA, ECDSA or AES keys as JSON web keys. 4 | #' 5 | #' @export 6 | #' @rdname jwk 7 | #' @name jwk 8 | #' @aliases jwk_write 9 | #' @param x an RSA or EC key or pubkey file 10 | #' @param path file path to write output 11 | #' @examples # generate an ecdsa key 12 | #' library(openssl) 13 | #' key <- ec_keygen("P-521") 14 | #' write_jwk(key) 15 | #' write_jwk(as.list(key)$pubkey) 16 | #' 17 | #' # Same for RSA 18 | #' key <- rsa_keygen() 19 | #' write_jwk(key) 20 | #' write_jwk(as.list(key)$pubkey) 21 | write_jwk <- function(x, path = NULL){ 22 | str <- jwk_export(x) 23 | if(is.null(path)) return(str) 24 | writeLines(str, path) 25 | invisible(path) 26 | } 27 | 28 | # Old name 29 | #' @export 30 | jwk_write <- write_jwk 31 | 32 | jwk_export <- function(x, ...){ 33 | UseMethod("jwk_export") 34 | } 35 | 36 | jwk_export.dsa <- function(x, ...){ 37 | stop("JWK does not support DSA keys. Try RSA or ECDSA instead") 38 | } 39 | 40 | jwk_export.ecdsa <- function(x, ...){ 41 | keydata <- as.list(x)$data 42 | out <- list ( 43 | kty = "EC", 44 | crv = keydata$curve, 45 | x = base64url_encode(keydata$x), 46 | y = base64url_encode(keydata$y) 47 | ) 48 | if(length(keydata$secret)) 49 | out$d <- base64url_encode(keydata$secret) 50 | to_json(out) 51 | } 52 | 53 | jwk_export.rsa <- function(x, ...){ 54 | keydata <- as.list(x)$data 55 | out <- lapply(keydata, base64url_encode) 56 | out$kty <- "RSA" 57 | to_json(out) 58 | } 59 | 60 | jwk_export.raw <- function(x, ...){ 61 | if(is.na(match(length(x), c(16, 24, 32, 48, 64)))) 62 | stop("Raw key must length 16, 24, 32 (AES) or 32, 48, 64 (HMAC)") 63 | to_json(list( 64 | kty = "oct", 65 | k = base64url_encode(x) 66 | )) 67 | } 68 | -------------------------------------------------------------------------------- /tests/keys/rsa-pkcs1.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "RS256", 3 | "d": "Jy2oaqw7BUhbooKRgNAPRR1A_-sOa6-xCcsLbKOXu1E4VH10B1OvySBuuDvurwzrAZYVbPJ5yR2YOyzu8eFgjuVQ6oGxbBGWA_ZA-27KinLXHuZ3lJJqOLFejtTDBgJQHyxfxZtcxRmJal4SM_pWbiQWi8JhkbmD2-AYajMUiUkIcKJRho3ghrQqywKf4q7Dx2GA1jqglrxNFTslZLjrKPDPFqEdGJh2TnApEwU1KLsft-KEkrUC2fSnvhOgsYGRMc5NxDpT9zN1pV3Keki1XtR3DNhREPXxIptcqM4npAFGFJXYAsJbcElgRkKam_Hi1QuX3M0Fuby9C3brNwN-4Q", 4 | "dp": "qNhU3W3bCk8U9ZLFj5CGe-lb14qgYO8Cjnh7_uTx1H3aiTXPK2GSDMeDHB246OiWqpewBx45wqclvCmtrS_PkwB_2O_Hpe3oQSE-c1M5Powr3iuOSUWuLjbXQ-XQ15Q66dATglmiPrByaMRQrmS5e9rLqjZXt3ufEoMkFY8MHpk", 5 | "dq": "HEUQXhUOjfbv4q9lmVCiRoJ1Ut1KWH7xuAPgUb4cF9RZ3jbRw1ijZIwDVoErs_ZTg2Wsp0JXTPVPul9WzH6TUd6EtjFGNFW7LVnUiGTxiDn4XBt2vZsjogCdXd7NZgh3vnkcGcWCPlzOVLG2CbQR0pGJ6623jPF0oEfQsSog-HE", 6 | "e": "AQAB", 7 | "ext": true, 8 | "key_ops": [ 9 | "sign" 10 | ], 11 | "kty": "RSA", 12 | "n": "rG2s5sqC_dciWLZMxFrXf-Wy5g8QzxeDa7266zLOujOIydwlsoegXCaTRk7A1nm3qpxkYdpUIO2uAmR4QIIl3E7fLZ6o9HKIJk64JqSOtibG3i1TRtiyS703CKK7rTpvvIx4uBKbl4PITC8u_EdaJMyF4q_kkGTFG7qwQNaHgqwb02A46W3ZbUQbyzLKZe3TpViMm0G8yz5Z2EOY9ijK2nByfaMa0dB53t84KFduYrxqGvuC8BVTUOJ67j-9A9kd2vkgPlyqz5beZP12pUtpDnkw2V9Q_4_fcnHp_qeItpeGTbwcGmeWTdWGGXM0WdyDNjqapEzkPs1vB894Glqsxw", 13 | "p": "1U8BY_QyuyPIrHs6ie3ourvWtax2ahOPKEN7OtkEr3EpOx8DLxUCf66YISqQK28cYZVXYV2g0jB7WRS3Nx6jp0CKxmEAj6awqaXJ-eOZDFMJJqCK6PvVEPscp8UzNfkGSYYx36cd0-pe_evLRQ4nDB8RQdc6W835KTT-ST-g2Dc", 14 | "q": "zvAmD_79JCgVjISRnqxooPJbIzdrtOR4aD3FqGalnQVlREXh7JFVrBbB4WtRfvxahRv-p3k5mK377j7EqcWbMOtX45PQAivZUKJ3aftsQsfjH1zA-zit_0N96Ye2tZ-wl-odE5NYF_3YwUuJfZ2REw3EgKleEFPEddGmEnAXZ_E", 15 | "qi": "ocZLSmnbPtxjT3VMpzpZ5YzXriiTa0rOBFXaYbr6YScf3eHw6HfzRy-n6eBeGlpFEYdV7_7vyA8ZlIWgVdTI0HwbnwDKI_p6nzynF6A15jW96Ji5QE1eTjNBpwxnpzG0AXiG9sp4sa6XyoS0kaBJEoDxyxCo_2Qbu2VceaZa9Ec" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/keys/rsa-oaep.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "RSA-OAEP-256", 3 | "d": "t2mVHGrECBlTjbtIzx9BpypNr_mqwF8Ex-90VLbJjLqG0ndHJysNvl6DUqj1jmYfRyYdwf1jLRJMM8AlqdIP2z_rjSiifRCQ1ENn5ixCQHD0Bk3zhj_tt2HrqFQbTCWiwDrEUBWCvJSWVnrILh_KIfTGp5wPq2iqsYLjT_poPD97wLJJFU2TRJH_XEZZrRwKUtehxSIiS90SL6rcZaIxsBvihYOGSLOsejusrRJl11e9vNbMNaGYKbk_rZjiVE5WTBuSyjCbj0RlV12HR9poj-yXBe26tjPrXRleDe7W1iBzFgjY9u3_cWtXpByGInWFFSOuoFWkVd6zqmuD9d5jkQ", 4 | "dp": "Q842SbYmyjx-RLftfQYBNlSCM14AWjj_kp1dGtKg36pZ-Gm5jYloiqSYMV67wOjE6GHzuRrkHfDoY49xlBrXKCj-qhOhfobiWOQfTbSR4qGDFrGyynSUdHFsVaMwAkttr4kBw1tfHDOjOusfYmLbucYT95UO3QJMc0xdT1px5SE", 5 | "dq": "q-9iroLPYeH9ELRnOXiznOiS0IIIwKi6e7E62jwUCNNXDfkSPc4tL4v3dGDOVpBuDxJXze8YYbS7eb6_5WzTEywy7v5XMvX-DOEYijEHAAMREPL1a0yiFDFvhF2_2y79Hv59N2MIlihCuvROpxByikF_h33Uqy_6xordMnaS6xU", 6 | "e": "AQAB", 7 | "ext": true, 8 | "key_ops": [ 9 | "decrypt" 10 | ], 11 | "kty": "RSA", 12 | "n": "xqL9-CoruIuemD1aF8Z4MC0IQkvnxtiRn67CkQPo967tHHIgd1dYYS6z3aIaNYFMIK6EclYaQYbuO3-JUco2hvvF4OYQ5SH-yP87EmcsZaUzOjGSR9A62cw3LJzyfTUnpaCqWI8aE6SrQrfzPq-y-tNnynihvEOLHTSAyUp7Vi3xxAnQ_YeydqTDFge2cfUJGLXHKve_uF9coTUNYWjMs8szcWnTg1m3LMaclFM7tfovNiNtkP1hWASaUOk-YTISQujt-eFroXyN-F6B3xyVcQ-7z-7s-KznsY4MxybCsioCAPwpkU_UoByxcrpBepyfhhAU43XmfuWilEc8n-A3Gw", 13 | "p": "49bCyBdRRzxiTLv8fq5InV8JIjvmsmtxWMsTAhi5d6RaVv3s_Ax4Y0vZpZPO2nfH8WOz6YLMdDNTOM4V4y6PQ9Wq_KuEwOXCrLYvRp0UrOGftsUcuHvj-jJphEyEYIs8NMzGaPdsCyqfzBs57ff-shxSSuGiJ0lqwfQJmqr2i0c", 14 | "q": "3zA4Nlggb2Xt3vo7_q5ndXOX5UrVWsbLSHE_8_JhtxyiFW3V6xnexqRakBkVN6IyY6O6vKGfTrd9vQdLd4C1uxEAltOl-Qn9OckSepCMhabLCZvjQD1m5vGSmHBtPvnpZ79xKQR3kjHzNTtCHM6A9ZmxuZN6xQ6zE4420e6s940", 15 | "qi": "fKgmoCiPNHPa3LD1LOI9bKdmoNJWxMWyvXHPIoalhJ7STAQA1QwXRplJYgoPCZ3NVLls12ntWCQJHhPoHykbhWpNfbG7io1PN2VL2yNzN0bAvUBKL6Ajw8zI6pC4V72zcU2D9ikcGr55ycmU8o26X8c7x1Y5rfrB2Xv7hjXMCog" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/testthat/test_sizes.R: -------------------------------------------------------------------------------- 1 | context("Hash sizes") 2 | 3 | test <- jwt_claim(session = "123456") 4 | 5 | test_that("HMAC sizes", { 6 | secret <- "This is a secret" 7 | sig <- jwt_encode_hmac(test, secret) 8 | sig256 <- jwt_encode_hmac(test, secret, size = 256) 9 | sig384 <- jwt_encode_hmac(test, secret, size = 384) 10 | sig512 <- jwt_encode_hmac(test, secret, size = 512) 11 | expect_equal(sig, sig256) 12 | expect_gt(nchar(sig384), nchar(sig256)) 13 | expect_gt(nchar(sig512), nchar(sig384)) 14 | expect_equal(test, jwt_decode_hmac(sig, secret)) 15 | expect_equal(test, jwt_decode_hmac(sig256, secret)) 16 | expect_equal(test, jwt_decode_hmac(sig384, secret)) 17 | expect_equal(test, jwt_decode_hmac(sig512, secret)) 18 | }) 19 | 20 | 21 | test_that("RSA sizes", { 22 | key <- openssl::rsa_keygen() 23 | pubkey <- as.list(key)$pubkey 24 | sig <- jwt_encode_sig(test, key) 25 | sig256 <- jwt_encode_sig(test, key, size = 256) 26 | sig384 <- jwt_encode_sig(test, key, size = 384) 27 | sig512 <- jwt_encode_sig(test, key, size = 512) 28 | expect_equal(test, jwt_decode_sig(sig, pubkey)) 29 | expect_equal(test, jwt_decode_sig(sig256, pubkey)) 30 | expect_equal(test, jwt_decode_sig(sig384, pubkey)) 31 | expect_equal(test, jwt_decode_sig(sig512, pubkey)) 32 | }) 33 | 34 | test_that("EC sizes", { 35 | key256 <- openssl::ec_keygen("P-256") 36 | key384 <- openssl::ec_keygen("P-384") 37 | key521 <- openssl::ec_keygen("P-521") 38 | pubkey256 <- as.list(key256)$pubkey 39 | pubkey384 <- as.list(key384)$pubkey 40 | pubkey521 <- as.list(key521)$pubkey 41 | sig <- jwt_encode_sig(test, key256) 42 | sig256 <- jwt_encode_sig(test, key256) 43 | sig384 <- jwt_encode_sig(test, key384) 44 | sig512 <- jwt_encode_sig(test, key521) 45 | expect_equal(test, jwt_decode_sig(sig, pubkey256)) 46 | expect_equal(test, jwt_decode_sig(sig256, pubkey256)) 47 | expect_equal(test, jwt_decode_sig(sig384, pubkey384)) 48 | expect_equal(test, jwt_decode_sig(sig512, pubkey521)) 49 | }) 50 | 51 | -------------------------------------------------------------------------------- /R/read.R: -------------------------------------------------------------------------------- 1 | #' @rdname jwk 2 | #' @aliases jwk_read 3 | #' @param file path to file with key data or literal json string 4 | #' @importFrom jsonlite fromJSON validate 5 | #' @export 6 | read_jwk <- function(file){ 7 | jwk <- if(is.character(file)){ 8 | if(validate(file)){ 9 | fromJSON(file) 10 | } else { 11 | fromJSON(rawToChar(openssl:::read_input(file))) 12 | } 13 | } else { 14 | file 15 | } 16 | if(!is.list(jwk) || !length(jwk$kty)) 17 | stop("File does not have jwk data") 18 | key <- switch(tolower(jwk$kty), 19 | "ec" = jwk_parse_ec(jwk), 20 | "rsa" = jwk_parse_rsa(jwk), 21 | "oct" = return(jwk_parse_oct(jwk)), #oct is just bytes 22 | stop("Unknown key type: ", jwk$kty) 23 | ) 24 | pubkey <- if(inherits(key, "key")){ 25 | openssl:::derive_pubkey(key) 26 | } else { 27 | key 28 | } 29 | type <- openssl:::pubkey_type(pubkey) 30 | structure(key, class = c(class(key), type)) 31 | } 32 | 33 | # Former name 34 | #' @export 35 | jwk_read <- read_jwk 36 | 37 | jwk_parse_ec <- function(input){ 38 | curve <- toupper(input$crv) 39 | x <- bignum(base64url_decode(input$x)) 40 | y <- bignum(base64url_decode(input$y)) 41 | if(length(input$d)){ 42 | d <- bignum(base64url_decode(input$d)) 43 | key <- openssl:::ecdsa_key_build(x, y, d, curve) 44 | structure(key, class = "key") 45 | } else { 46 | pubkey <- openssl:::ecdsa_pubkey_build(x, y, curve) 47 | structure(pubkey, class = "pubkey") 48 | } 49 | } 50 | 51 | #' @importFrom openssl bignum 52 | jwk_parse_rsa <- function(input){ 53 | e <- bignum(base64url_decode(input$e)) 54 | n <- bignum(base64url_decode(input$n)) 55 | if(length(input$d)){ 56 | p <- bignum(base64url_decode(input$p)) 57 | q <- bignum(base64url_decode(input$q)) 58 | d <- bignum(base64url_decode(input$d)) 59 | key <- openssl:::rsa_key_build(e, n, p, q, d) 60 | structure(key, class = "key") 61 | } else { 62 | pubkey <- openssl:::rsa_pubkey_build(e, n) 63 | structure(pubkey, class = "pubkey") 64 | } 65 | } 66 | 67 | jwk_parse_oct <- function(input){ 68 | base64url_decode(input$k) 69 | } 70 | -------------------------------------------------------------------------------- /tests/js/rsa.js: -------------------------------------------------------------------------------- 1 | //See: https://github.com/diafygi/webcrypto-examples 2 | function str2buf(str) { 3 | var bufView = new Uint8Array(str.length); 4 | for (var i=0, strLen=str.length; i 1 || val > max) 53 | stop("Invalid 'NumericDate' (seconds since epoch) value: ", val) 54 | round(val) 55 | } 56 | -------------------------------------------------------------------------------- /man/jwt_encode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jwt.R 3 | \name{jwt_encode_hmac} 4 | \alias{jwt_encode_hmac} 5 | \alias{jwt} 6 | \alias{jose} 7 | \alias{jwt_decode_hmac} 8 | \alias{jwt_encode_sig} 9 | \alias{jwt_decode_sig} 10 | \alias{jwt_split} 11 | \title{JSON Web Token} 12 | \usage{ 13 | jwt_encode_hmac(claim = jwt_claim(), secret, size = 256, header = NULL) 14 | 15 | jwt_decode_hmac(jwt, secret) 16 | 17 | jwt_encode_sig(claim = jwt_claim(), key, size = 256, header = NULL) 18 | 19 | jwt_decode_sig(jwt, pubkey) 20 | 21 | jwt_split(jwt) 22 | } 23 | \arguments{ 24 | \item{claim}{a named list with fields to include in the jwt payload} 25 | 26 | \item{secret}{string or raw vector with a secret passphrase} 27 | 28 | \item{size}{bitsize of sha2 signature, i.e. \code{sha256}, \code{sha384} or \code{sha512}. 29 | Only for HMAC/RSA, not applicable for ECDSA keys.} 30 | 31 | \item{header}{named list with additional parameter fields to include in the jwt header as 32 | defined in \href{https://datatracker.ietf.org/doc/html/rfc7515#section-9.1.2}{rfc7515 section 9.1.2}} 33 | 34 | \item{jwt}{string containing the JSON Web Token (JWT)} 35 | 36 | \item{key}{path or object with RSA or EC private key, see \link[openssl:read_key]{openssl::read_key}.} 37 | 38 | \item{pubkey}{path or object with RSA or EC public key, see \link[openssl:read_pubkey]{openssl::read_pubkey}.} 39 | } 40 | \description{ 41 | Sign or verify a JSON web token. The \code{jwt_encode_hmac}, \code{jwt_encode_rsa}, 42 | and \code{jwt_encode_ec} default to \code{HS256}, \code{RS256}, and \code{ES256} 43 | respectively. See \href{https://jwt.io}{jwt.io} or 44 | \href{https://datatracker.ietf.org/doc/html/rfc7519}{RFC7519} for more details. 45 | } 46 | \examples{ 47 | # HMAC signing 48 | mysecret <- "This is super secret" 49 | token <- jwt_claim(name = "jeroen", session = 123456) 50 | sig <- jwt_encode_hmac(token, mysecret) 51 | jwt_decode_hmac(sig, mysecret) 52 | 53 | # RSA encoding 54 | mykey <- openssl::rsa_keygen() 55 | pubkey <- as.list(mykey)$pubkey 56 | sig <- jwt_encode_sig(token, mykey) 57 | jwt_decode_sig(sig, pubkey) 58 | 59 | # Same with EC 60 | mykey <- openssl::ec_keygen() 61 | pubkey <- as.list(mykey)$pubkey 62 | sig <- jwt_encode_sig(token, mykey) 63 | jwt_decode_sig(sig, pubkey) 64 | 65 | # Get elements of the key 66 | mysecret <- "This is super secret" 67 | token <- jwt_claim(name = "jeroen", session = 123456) 68 | jwt <- jwt_encode_hmac(token, mysecret) 69 | jwt_split(jwt) 70 | } 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # jose 2 | 3 | [![Build Status](https://app.travis-ci.com/jeroen/jose.svg?branch=master)](https://app.travis-ci.com/jeroen/jose) 4 | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/jeroen/jose?branch=master&svg=true)](https://ci.appveyor.com/project/jeroen/jose) 5 | [![Coverage Status](https://codecov.io/github/jeroen/jose/coverage.svg?branch=master)](https://app.codecov.io/github/jeroen/jose?branch=master) 6 | [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/jose)](http://cran.r-project.org/package=jose) 7 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/jose)](http://cran.r-project.org/web/packages/jose/index.html) 8 | 9 | > JavaScript Object Signing and Encryption 10 | 11 | Read and write JSON Web Keys (JWK, rfc7517), generate and verify JSON 12 | Web Signatures (JWS, rfc7515) and encode/decode JSON Web Tokens (JWT, rfc7519). 13 | These standards provide modern signing and encryption formats that are natively 14 | supported by browsers via the JavaScript WebCryptoAPI, and used by services 15 | like OAuth 2.0, LetsEncrypt, and Github Apps. 16 | 17 | ## Documentation 18 | 19 | Vignettes for the R package: 20 | 21 | - [Reading/Writing JSON Web Keys (JWK) in R](https://cran.r-project.org/web/packages/jose/vignettes/jwk.html) 22 | - [Encoding/Decoding JSON Web Tokens (JWT) in R](https://cran.r-project.org/web/packages/jose/vignettes/jwt.html) 23 | 24 | Specifications and standards: 25 | 26 | - JOSE RFC Tracker: https://datatracker.ietf.org/wg/jose/documents/ 27 | - Browser WebCryptoAPI API: https://www.w3.org/TR/WebCryptoAPI/#jose 28 | - ACME Protocol (LetsEncrypt): https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html 29 | 30 | ## JSON Web Keys (JWK) 31 | 32 | ```r 33 | library(jose) 34 | 35 | # generate an ecdsa key 36 | key <- ec_keygen("P-521") 37 | write_jwk(key) 38 | write_jwk(as.list(key)$pubkey) 39 | 40 | # Same for RSA 41 | key <- rsa_keygen() 42 | write_jwk(key) 43 | write_jwk(as.list(key)$pubkey) 44 | ``` 45 | 46 | ## JSON Web Tokens (JWT) 47 | 48 | ```r 49 | # HMAC signing 50 | mysecret <- "This is super secret" 51 | token <- jwt_claim(name = "jeroen", session = 123456) 52 | sig <- jwt_encode_hmac(token, mysecret) 53 | jwt_decode_hmac(sig, mysecret) 54 | 55 | # RSA encoding 56 | mykey <- openssl::rsa_keygen() 57 | pubkey <- as.list(mykey)$pubkey 58 | sig <- jwt_encode_sig(token, mykey) 59 | jwt_decode_sig(sig, pubkey) 60 | 61 | # Same with EC 62 | mykey <- openssl::ec_keygen() 63 | pubkey <- as.list(mykey)$pubkey 64 | sig <- jwt_encode_sig(token, mykey) 65 | jwt_decode_sig(sig, pubkey) 66 | ``` 67 | -------------------------------------------------------------------------------- /vignettes/jwt.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Encoding/Decoding JSON Web Tokens (JWT) in R" 3 | date: "`r Sys.Date()`" 4 | output: 5 | html_document 6 | vignette: > 7 | %\VignetteIndexEntry{Encoding/Decoding JSON Web Tokens (JWT) in R} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include=FALSE} 13 | knitr::opts_chunk$set(echo = TRUE) 14 | knitr::opts_chunk$set(comment = "") 15 | ``` 16 | 17 | JavaScript Object Signing and Encryption (JOSE) consists of a set of specifications for encryption and signatures based on the popular JSON format. This is work in progress, the IETF [jose workgroup](https://datatracker.ietf.org/wg/jose/) usually has the latest information. 18 | 19 | - [RFC7515](https://datatracker.ietf.org/doc/html/rfc7515): JSON Web Signature (JWS) 20 | - [RFC7516](https://datatracker.ietf.org/doc/html/rfc7516): JSON Web Encryption (JWE) 21 | - [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517): JSON Web Key (JWK) 22 | - [RFC7518](https://datatracker.ietf.org/doc/html/rfc7518): JSON Web Algorithms (JWA) 23 | - [RFC7519](https://datatracker.ietf.org/doc/html/rfc7519): JSON Web Token (JWT) 24 | 25 | The `jose` package implements some of these specifications, in particular for working with JSON web tokens and keys. 26 | 27 | ### JSON Web Token: HMAC tagging 28 | 29 | The most common use of JSON Web Tokens is combining a small payload (the 'claim') with a HMAC tag or RSA/ECDSA signature. See also [https://jwt.io](https://jwt.io) for short introduction. 30 | 31 | ```{r} 32 | library(openssl) 33 | library(jose) 34 | 35 | # Example payload 36 | claim <- jwt_claim(user = "jeroen", session_key = 123456) 37 | 38 | # Encode with hmac 39 | key <- charToRaw("SuperSecret") 40 | (jwt <- jwt_encode_hmac(claim, secret = key)) 41 | 42 | # Decode 43 | jwt_decode_hmac(jwt, secret = key) 44 | ``` 45 | 46 | The decoding errors if the tag verification fails. 47 | 48 | ```{r error=TRUE} 49 | # What happens if we decode with the wrong key 50 | jwt_decode_hmac(jwt, secret = raw()) 51 | ``` 52 | 53 | ### JSON Web Token: RSA/ECDSA signature 54 | 55 | Similarly, we can use an RSA or ECDSA key pair we to verify a signature from someone's public key. 56 | 57 | ```{r} 58 | # Generate ECDSA keypair 59 | key <- ec_keygen() 60 | pubkey <- as.list(key)$pubkey 61 | 62 | # Sign with the private key 63 | (jwt <- jwt_encode_sig(claim, key = key)) 64 | 65 | # Decode and verify using the public key 66 | jwt_decode_sig(jwt, pubkey = pubkey) 67 | ``` 68 | 69 | Again decoding will error if the signature verification fails. 70 | 71 | ```{r error = TRUE} 72 | wrong_key <- ec_keygen() 73 | jwt_decode_sig(jwt, pubkey = wrong_key) 74 | ``` 75 | 76 | 77 | The spec also describes methods for encrypting the payload, but this is currently not widely in use yet. 78 | 79 | ### Reserved jwt-claim names 80 | 81 | You can include custom fields in your jwt payload, but the spec names a few [registered claims](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1) that are reserved for specific uses. 82 | 83 | - `iss` (Issuer): the principal that issued the JWT. 84 | - `sub` (Subject): the principal that is the subject of the JWT. 85 | - `aud` (Audience): the recipients that the JWT is intended for. 86 | - `exp` (Expiration Time): the expiration time on or after which the JWT must not be accepted. 87 | - `nbf` (Not Before): the time before which the JWT must not be accepted. 88 | - `iat` (Issued At): the time at which the JWT was issued. 89 | - `jti` (JWT ID): a unique identifier for the JWT. 90 | 91 | Each of these are optional, by default only `iat` is set. The `jwt_claim()` function will automatically do basic validation when you set additional fields from this list. For any other fields you can use any value. For example: 92 | 93 | ```{r} 94 | # Note that this token expires in 1 hour! 95 | myclaim <- jwt_claim( 96 | iss = "My webapp", 97 | exp = Sys.time() + 3600, 98 | myfield = "Some application logic", 99 | customer = "a cow" 100 | ) 101 | (jwt <- jwt_encode_sig(myclaim, key = key)) 102 | ``` 103 | 104 | The decode functions will automatically verify that the token has not expired (with a 60s grace period to account for inaccurate clocks), and error otherwise: 105 | 106 | ```{r} 107 | jwt_decode_sig(jwt, pubkey = pubkey) 108 | ``` 109 | 110 | 111 | ### Where is the JSON 112 | 113 | The jwt payloads consists of a head, body and signature which are separated with a dot into a single string. Both the header and body are actually `base64url` encoded JSON objects. 114 | 115 | ```{r} 116 | (strings <- strsplit(jwt, ".", fixed = TRUE)[[1]]) 117 | cat(rawToChar(base64url_decode(strings[1]))) 118 | cat(rawToChar(base64url_decode(strings[2]))) 119 | ``` 120 | 121 | However you should never trust this information without verifying the signature. This is what the `jwt_decode` functions do for you. 122 | -------------------------------------------------------------------------------- /tests/testthat/test_examples.R: -------------------------------------------------------------------------------- 1 | context("Examples from JWT website") 2 | 3 | test_that("HMAC example", { 4 | buf <- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" 5 | key <- "secret" 6 | expect_equal(jwt_decode_hmac (buf, key)$sub, "1234567890") 7 | expect_error(jwt_decode_hmac (buf, "bla"), "verif") 8 | }) 9 | 10 | 11 | test_that("RSA 256 example", { 12 | pubkeystring <- "-----BEGIN PUBLIC KEY----- 13 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 14 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 15 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 16 | o2kQ+X5xK9cipRgEKwIDAQAB 17 | -----END PUBLIC KEY-----" 18 | sig <- "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE" 19 | pk <- openssl::read_pubkey(pubkeystring) 20 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 21 | expect_error(jwt_decode_sig(sig, openssl::rsa_keygen()), "fail") 22 | }) 23 | 24 | test_that("RSA 384 example", { 25 | pubkeystring <- "-----BEGIN PUBLIC KEY----- 26 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 27 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 28 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 29 | o2kQ+X5xK9cipRgEKwIDAQAB 30 | -----END PUBLIC KEY-----" 31 | sig <-'eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.CN9hqUMdVb5LGo06Geb8ap1qYfbJ4rEZIMqTE9gxA2m6GGmsXkznRxzoFpAzQUey9q5HehRTk_-TxYydN3QtFPfrTbAHep7PLhp3XhdvTJ1ok__UBjv4aP6UWTF-Rflr3qeC18LdlM4nyKL7ZwSGDzytWihGod5vn4GAXErUUE4' 32 | pk <- openssl::read_pubkey(pubkeystring) 33 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 34 | expect_error(jwt_decode_sig(sig, openssl::rsa_keygen()), "fail") 35 | }) 36 | 37 | test_that("RSA 512 example", { 38 | pubkeystring <- "-----BEGIN PUBLIC KEY----- 39 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd 40 | UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs 41 | HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D 42 | o2kQ+X5xK9cipRgEKwIDAQAB 43 | -----END PUBLIC KEY-----" 44 | sig <- 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.MejLezWY6hjGgbIXkq6Qbvx_-q5vWaTR6qPiNHphvla-XaZD3up1DN6Ib5AEOVtuB3fC9l-0L36noK4qQA79lhpSK3gozXO6XPIcCp4C8MU_ACzGtYe7IwGnnK3Emr6IHQE0bpGinHX1Ak1pAuwJNawaQ6Nvmz2ozZPsyxmiwoo' 45 | pk <- openssl::read_pubkey(pubkeystring) 46 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 47 | expect_error(jwt_decode_sig(sig, openssl::rsa_keygen()), "fail") 48 | }) 49 | 50 | test_that("ECDSA 256 example", { 51 | pubkeystring <- '-----BEGIN PUBLIC KEY----- 52 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 53 | q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== 54 | -----END PUBLIC KEY-----' 55 | 56 | sig <- 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA' 57 | pk <- openssl::read_pubkey(pubkeystring) 58 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 59 | expect_error(jwt_decode_sig(sig, openssl::ec_keygen()), "fail") 60 | }) 61 | 62 | test_that("ECDSA 384 example", { 63 | pubkeystring <- '-----BEGIN PUBLIC KEY----- 64 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+ 65 | Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii 66 | 1D3jaW6pmGVJFhodzC31cy5sfOYotrzF 67 | -----END PUBLIC KEY-----' 68 | 69 | sig <- 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImlUcVhYSTB6YkFuSkNLRGFvYmZoa00xZi02ck1TcFRmeVpNUnBfMnRLSTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.cJOP_w-hBqnyTsBm3T6lOE5WpcHaAkLuQGAs1QO-lg2eWs8yyGW8p9WagGjxgvx7h9X72H7pXmXqej3GdlVbFmhuzj45A9SXDOAHZ7bJXwM1VidcPi7ZcrsMSCtP1hiN' 70 | pk <- openssl::read_pubkey(pubkeystring) 71 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 72 | expect_error(jwt_decode_sig(sig, openssl::ec_keygen()), "fail") 73 | }) 74 | 75 | test_that("ECDSA 512 example", { 76 | pubkeystring <- '-----BEGIN PUBLIC KEY----- 77 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ 78 | PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47 79 | 6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM 80 | Al8G7CqwoJOsW7Kddns= 81 | -----END PUBLIC KEY-----' 82 | 83 | sig <- 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AP_CIMClixc5-BFflmjyh_bRrkloEvwzn8IaWJFfMz13X76PGWF0XFuhjJUjp7EYnSAgtjJ-7iJG4IP7w3zGTBk_AUdmvRCiWp5YAe8S_Hcs8e3gkeYoOxiXFZlSSAx0GfwW1cZ0r67mwGtso1I3VXGkSjH5J0Rk6809bn25GoGRjOPu' 84 | pk <- openssl::read_pubkey(pubkeystring) 85 | expect_equal(jwt_decode_sig(sig, pk)$sub, "1234567890") 86 | expect_error(jwt_decode_sig(sig, openssl::ec_keygen()), "fail") 87 | }) 88 | -------------------------------------------------------------------------------- /R/jwt.R: -------------------------------------------------------------------------------- 1 | #' JSON Web Token 2 | #' 3 | #' Sign or verify a JSON web token. The \code{jwt_encode_hmac}, \code{jwt_encode_rsa}, 4 | #' and \code{jwt_encode_ec} default to \code{HS256}, \code{RS256}, and \code{ES256} 5 | #' respectively. See \href{https://jwt.io}{jwt.io} or 6 | #' \href{https://tools.ietf.org/html/rfc7519}{RFC7519} for more details. 7 | #' 8 | #' @export 9 | #' @rdname jwt_encode 10 | #' @aliases jwt jose 11 | #' @param claim a named list with fields to include in the jwt payload 12 | #' @param secret string or raw vector with a secret passphrase 13 | #' @param size bitsize of sha2 signature, i.e. \code{sha256}, \code{sha384} or \code{sha512}. 14 | #' Only for HMAC/RSA, not applicable for ECDSA keys. 15 | #' @param header named list with additional parameter fields to include in the jwt header as 16 | #' defined in \href{https://tools.ietf.org/html/rfc7515#section-9.1.2}{rfc7515 section 9.1.2} 17 | #' @param jwt string containing the JSON Web Token (JWT) 18 | #' @param key path or object with RSA or EC private key, see \link[openssl:read_key]{openssl::read_key}. 19 | #' @param pubkey path or object with RSA or EC public key, see \link[openssl:read_pubkey]{openssl::read_pubkey}. 20 | #' @importFrom openssl sha2 signature_create signature_verify read_pubkey read_key 21 | #' @importFrom jsonlite fromJSON toJSON 22 | #' @examples # HMAC signing 23 | #' mysecret <- "This is super secret" 24 | #' token <- jwt_claim(name = "jeroen", session = 123456) 25 | #' sig <- jwt_encode_hmac(token, mysecret) 26 | #' jwt_decode_hmac(sig, mysecret) 27 | #' 28 | #' # RSA encoding 29 | #' mykey <- openssl::rsa_keygen() 30 | #' pubkey <- as.list(mykey)$pubkey 31 | #' sig <- jwt_encode_sig(token, mykey) 32 | #' jwt_decode_sig(sig, pubkey) 33 | #' 34 | #' # Same with EC 35 | #' mykey <- openssl::ec_keygen() 36 | #' pubkey <- as.list(mykey)$pubkey 37 | #' sig <- jwt_encode_sig(token, mykey) 38 | #' jwt_decode_sig(sig, pubkey) 39 | #' 40 | #' # Get elements of the key 41 | #' mysecret <- "This is super secret" 42 | #' token <- jwt_claim(name = "jeroen", session = 123456) 43 | #' jwt <- jwt_encode_hmac(token, mysecret) 44 | #' jwt_split(jwt) 45 | jwt_encode_hmac <- function(claim = jwt_claim(), secret, size = 256, header = NULL) { 46 | stopifnot(inherits(claim, "jwt_claim")) 47 | if(is.character(secret)) 48 | secret <- charToRaw(secret) 49 | if(!is.raw(secret)) 50 | stop("Secret must be a string or raw vector") 51 | if(inherits(secret, "rsa") || inherits(secret, "dsa") || inherits(secret, "ecdsa")) 52 | stop("Secret must be raw bytes, not a: ", class(secret)[-1]) 53 | jwt_header <- to_json(c(list( 54 | typ = "JWT", 55 | alg = paste0("HS", size) 56 | ), header)) 57 | body <- to_json(claim) 58 | doc <- paste(base64url_encode(jwt_header), base64url_encode(body), sep = ".") 59 | sig <- sha2(charToRaw(doc), size = size, key = secret) 60 | paste(doc, base64url_encode(sig), sep = ".") 61 | } 62 | 63 | #' @export 64 | #' @rdname jwt_encode 65 | jwt_decode_hmac <- function(jwt, secret){ 66 | if(is.character(secret)) 67 | secret <- charToRaw(secret) 68 | if(!is.raw(secret)) 69 | stop("Secret must be a string or raw vector") 70 | if(inherits(secret, "rsa") || inherits(secret, "dsa") || inherits(secret, "ecdsa")) 71 | stop("Secret must be raw bytes, not a: ", class(secret)[-1]) 72 | out <- jwt_split(jwt) 73 | if(out$type != "HMAC") 74 | stop("Invalid algorithm: ", out$type) 75 | sig <- sha2(out$data, size = out$keysize, key = secret) 76 | if(!identical(out$sig, unclass(sig))) 77 | stop("HMAC signature verification failed!", call. = FALSE) 78 | check_expiration_time(out$payload) 79 | structure(out$payload, class = c("jwt_claim", "list")) 80 | } 81 | 82 | #' @export 83 | #' @rdname jwt_encode 84 | jwt_encode_sig <- function(claim = jwt_claim(), key, size = 256, header = NULL) { 85 | stopifnot(inherits(claim, "jwt_claim")) 86 | key <- read_key(key) 87 | if(!inherits(key, "key")) 88 | stop("key must be rsa/ecdsa private key") 89 | # See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.4 90 | jwt_header <- if(inherits(key, "rsa")){ 91 | if(as.list(key)$size < 2048) 92 | stop("RSA keysize must be at least 2048 bit") 93 | to_json(utils::modifyList(list( 94 | typ = "JWT", 95 | alg = paste0("RS", size) 96 | ), as.list(header))) 97 | } else if(inherits(key, "ecdsa")){ 98 | # See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.4 99 | size <- switch(as.list(key)$data$curve, 100 | "P-256" = 256, "P-384" = 384, "P-521" = 512, stop("invalid curve")) 101 | to_json(utils::modifyList(list( 102 | typ = "JWT", 103 | alg = paste0("ES", size) 104 | ), as.list(header))) 105 | } else { 106 | stop("Key must be RSA or ECDSA private key") 107 | } 108 | doc <- paste(base64url_encode(jwt_header), base64url_encode(to_json(claim)), sep = ".") 109 | dgst <- sha2(charToRaw(doc), size = size) 110 | sig <- signature_create(dgst, hash = NULL, key = key) 111 | if(inherits(key, "ecdsa")){ 112 | params <- openssl::ecdsa_parse(sig) 113 | bitsize <- ceiling(size / 8) 114 | sig <- c(pad_bignum(params$r, size), pad_bignum(params$s, size)) 115 | } 116 | paste(doc, base64url_encode(sig), sep = ".") 117 | } 118 | 119 | #' @export 120 | #' @rdname jwt_encode 121 | jwt_decode_sig <- function(jwt, pubkey){ 122 | out <- jwt_split(jwt) 123 | if(out$type != "RSA" && out$type != "ECDSA") 124 | stop("Invalid algorithm: ", out$type) 125 | key <- read_pubkey(pubkey) 126 | if((!inherits(key, "rsa") && !inherits(key, "ecdsa")) || !inherits(key, "pubkey")) 127 | stop("Key must be rsa/ecdsa public key") 128 | dgst <- sha2(out$data, size = out$keysize) 129 | if(out$type == "ECDSA"){ 130 | bitsize <- length(out$sig)/2 131 | r <- out$sig[seq_len(bitsize)] 132 | s <- out$sig[seq_len(bitsize) + bitsize] 133 | out$sig <- openssl::ecdsa_write(r, s) 134 | } 135 | if(!signature_verify(dgst, out$sig, hash = NULL, pubkey = key)) 136 | stop(out$type, " signature verification failed!", call. = FALSE) 137 | check_expiration_time(out$payload) 138 | structure(out$payload, class = c("jwt_claim", "list")) 139 | } 140 | 141 | #' @export 142 | #' @rdname jwt_encode 143 | jwt_split <- function(jwt){ 144 | input <- strsplit(jwt, ".", fixed = TRUE)[[1]] 145 | stopifnot(length(input) %in% c(2,3)) 146 | header <- fromJSON(rawToChar(base64url_decode(input[1]))) 147 | stopifnot(toupper(header$typ) == "JWT") 148 | if(is.na(input[3])) input[3] = "" 149 | sig <- base64url_decode(input[3]) 150 | payload <- fromJSON(rawToChar(base64url_decode(input[2]))) 151 | data <- charToRaw(paste(input[1:2], collapse = ".")) 152 | if(!grepl("^none|[HRE]S(256|384|512)$", header$alg)) 153 | stop("Invalid algorithm: ", header$alg) 154 | keysize <- as.numeric(substring(header$alg, 3)) 155 | type <- match.arg(substring(header$alg, 1, 1), c("HMAC", "RSA", "ECDSA")) 156 | list(type = type, keysize = keysize, data = data, sig = sig, payload = payload, header = header) 157 | } 158 | 159 | to_json <- function(x){ 160 | jsonlite::toJSON(x, auto_unbox = TRUE) 161 | } 162 | 163 | # Adds leading zeros if needed (P512 is 521 bit == 66 bytes) 164 | # Spec: https://tools.ietf.org/html/rfc7518#page-10 165 | pad_bignum <- function(x, keysize){ 166 | stopifnot(keysize %in% c(256, 384, 512)) 167 | bitsize <- switch (as.character(keysize), "256" = 32, "384" = 48, "512" = 66) 168 | c(raw(bitsize - length(x)), x) 169 | } 170 | 171 | # As suggested in the spec, we give a 60s grace period to account 172 | # for inaccurate clocks. 173 | check_expiration_time <- function(payload){ 174 | if(length(payload$exp)){ 175 | stopifnot("exp claim is a number" = is.numeric(payload$exp)) 176 | expdate <- structure(payload$exp, class = c("POSIXct", "POSIXt")) 177 | if(expdate < (Sys.time() - 60)){ 178 | stop(paste("Token has expired on", expdate), call. = FALSE) 179 | } 180 | } 181 | if(length(payload$nbf)){ 182 | stopifnot("nbf claim is a number" = is.numeric(payload$nbf)) 183 | nbfdate <- structure(payload$nbf, class = c("POSIXct", "POSIXt")) 184 | if(nbfdate > (Sys.time() + 60)){ 185 | stop(paste("Token is not valid before", nbfdate), call. = FALSE) 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------