{"id":51879358,"url":"https://github.com/jsavyasachi/jose-clj","last_synced_at":"2026-07-25T10:03:17.713Z","repository":{"id":370810350,"uuid":"1294385363","full_name":"jsavyasachi/jose-clj","owner":"jsavyasachi","description":"Idiomatic Clojure JOSE (JWS/JWE/JWK/JWKS/JWT) over Nimbus JOSE+JWT","archived":false,"fork":false,"pushed_at":"2026-07-23T01:24:01.000Z","size":146,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-23T03:13:15.233Z","etag":null,"topics":["clojure","jose","jwe","jwk","jwks","jws","jwt","nimbus","security"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsavyasachi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-08T23:22:14.000Z","updated_at":"2026-07-23T01:24:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jsavyasachi/jose-clj","commit_stats":null,"previous_names":["jsavyasachi/jose-clj"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/jsavyasachi/jose-clj","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsavyasachi%2Fjose-clj","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsavyasachi%2Fjose-clj/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsavyasachi%2Fjose-clj/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsavyasachi%2Fjose-clj/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsavyasachi","download_url":"https://codeload.github.com/jsavyasachi/jose-clj/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsavyasachi%2Fjose-clj/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35875619,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-25T02:00:06.922Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","jose","jwe","jwk","jwks","jws","jwt","nimbus","security"],"created_at":"2026-07-25T10:03:16.840Z","updated_at":"2026-07-25T10:03:17.696Z","avatar_url":"https://github.com/jsavyasachi.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jose-clj\n\n[![Clojars Project](https://img.shields.io/clojars/v/net.clojars.savya/jose-clj.svg)](https://clojars.org/net.clojars.savya/jose-clj)\n[![cljdoc](https://cljdoc.org/badge/net.clojars.savya/jose-clj)](https://cljdoc.org/d/net.clojars.savya/jose-clj/CURRENT)\n[![test](https://github.com/jsavyasachi/jose-clj/actions/workflows/test.yml/badge.svg)](https://github.com/jsavyasachi/jose-clj/actions/workflows/test.yml)\n\nIdiomatic Clojure JOSE - JWS, JWE, JWK/JWKS, and JWT (signed, encrypted, and\nnested) - over [Nimbus JOSE+JWT](https://connect2id.com/products/nimbus-jose-jwt),\nthe canonical Java JOSE implementation. Maps in, maps out.\n\n## Stack\n\n\u003ca href=\"https://clojure.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/Clojure-5881D8?style=flat\u0026logo=clojure\u0026logoColor=fff\" alt=\"Clojure\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://connect2id.com/products/nimbus-jose-jwt\"\u003e\u003cimg src=\"https://img.shields.io/badge/Nimbus%20JOSE+JWT-2D3748?style=flat\" alt=\"Nimbus JOSE+JWT\" /\u003e\u003c/a\u003e\n\n\u003e Unofficial, community-maintained. Wraps Nimbus JOSE+JWT; not affiliated with\n\u003e Connect2id.\n\n## Why\n\nClojure's JOSE story is partial. buddy-sign covers JWS and signed JWTs well but\nits JWE support is limited and it has been dormant since 2024; other libraries do\nJWT validation or remote JWKS fetching but not encryption or key generation. There\nwas no single, idiomatic library covering the whole of JOSE - in particular full\nJWE, the JWK lifecycle (generate, thumbprint, sets), cached remote JWKS, and\nmodern algorithms (EdDSA, ECDH-ES). jose-clj wraps the maintained, complete Nimbus\nengine rather than reimplementing any crypto, so the primitives are as correct as\nNimbus and stay correct as it is updated.\n\n## Installation\n\ndeps.edn:\n\n```clojure\nnet.clojars.savya/jose-clj {:mvn/version \"0.5.0\"}\n```\n\nLeiningen:\n\n```clojure\n[net.clojars.savya/jose-clj \"0.5.0\"]\n```\n\nTracks `com.nimbusds/nimbus-jose-jwt` 10.9.1. Because jose-clj is a thin wrapper,\nNimbus updates (including security fixes) are picked up by bumping that one\ndependency; the weekly antq workflow proposes bumps automatically.\n\nJDK 11+. Some paths need optional engines on the classpath: EdDSA/Ed25519\nrequires `com.google.crypto.tink/tink`, ES256K (secp256k1) requires\nBouncyCastle (`org.bouncycastle/bcprov-jdk18on`), and PEM parsing requires\n`org.bouncycastle/bcpkix-jdk18on`. Everything else runs on the plain JDK. If you\ncall one of these paths without the engine present, the error is\n`{:jose/error :missing-optional-dep}` rather than a `NoClassDefFoundError`.\n\n## Usage\n\n```clojure\n(require '[jose.jwk :as jwk]\n         '[jose.jwt :as jwt]\n         '[jose.jwe :as jwe])\n\n;; a signing key\n(def k (jwk/generate :rsa {:kid \"sig-1\" :use :sig}))\n\n;; signed JWT with claims and an expiry\n(def token (jwt/sign k {:sub \"alice\" :iss \"me\"} {:expires-in 3600}))\n(jwt/verify k token {:iss \"me\"})\n;; =\u003e {:sub \"alice\" :iss \"me\" :exp 1751...}\n\n;; JWE round trip (ECDH-ES key agreement + A256GCM)\n(def ek (jwk/generate :ec {:curve :p-256}))\n(def ct (jwe/encrypt ek \"secret msg\" {:alg :ecdh-es :enc :a256gcm}))\n(:payload (jwe/decrypt ek ct))\n;; =\u003e \"secret msg\"\n\n(jwk/thumbprint k)   ;; =\u003e RFC 7638 SHA-256 thumbprint string\n```\n\n### Keys (`jose.jwk`)\n\n```clojure\n(jwk/generate :rsa {:size 2048 :kid \"k1\" :use :sig :alg :rs256})\n(jwk/generate :ec  {:curve :p-256})       ; :p-256 :p-384 :p-521 :secp256k1\n(jwk/generate :okp {:curve :ed25519})     ; :ed25519 (sign) :x25519 (encrypt)\n(jwk/generate :oct {:size 256})           ; symmetric\n\n(jwk/public-jwk k)      ; public-only counterpart (nil for :oct)\n(jwk/-\u003emap k)           ; JWK as a Clojure map (includes private params)\n(jwk/-\u003ejson k {:private? false})\n(jwk/thumbprint k)      ; RFC 7638\n(jwk/key-type k)        ; :rsa :ec :okp :oct\n\n;; JWK sets\n(def ks (jwk/jwk-set [k1 k2]))\n(jwk/find-key ks \"k1\")\n(jwk/set-\u003ejson ks)      ; public-only by default\n```\n\n### PEM keys (`jose.pem`)\n\n```clojure\n(require '[jose.pem :as pem])\n\n(def private-pem (pem/jwk-\u003epem k {:private? true})) ; PKCS#8 PRIVATE KEY\n(def public-pem  (pem/jwk-\u003epem k))                  ; SPKI PUBLIC KEY\n\n(pem/pem-\u003ejwk private-pem) ; =\u003e JWK map\n(pem/pem-\u003ejwk public-pem)  ; =\u003e public JWK map\n```\n\n`pem-\u003ejwk` accepts PEM public keys, private keys, and X.509 certificates. PEM\nparsing is a Nimbus path backed by BouncyCastle PKIX, so add\n`org.bouncycastle/bcpkix-jdk18on` at runtime when parsing PEM. EC private-key\nexport includes the matching public PEM block so Nimbus can reconstruct the full\nEC JWK from PKCS#8 private material.\n\n### Signing (`jose.jws`)\n\n```clojure\n(jws/sign k \"payload\")                 ; alg defaults per key type\n(jws/sign k \"payload\" {:alg :rs256 :kid \"k1\" :headers {:cty \"example\"}})\n(jws/verify k compact)                 ; =\u003e {:payload .. :payload-bytes .. :header ..}\n(jws/header compact)                   ; unverified header\n```\n\n### Detached / unencoded JWS\n\n```clojure\n(def detached (jws/sign k \"payload\" {:detached? true}))\n(jws/verify-detached k detached \"payload\")\n\n(def unencoded (jws/sign k \"payload\" {:b64? false}))\n(jws/verify k unencoded)\n\n(def detached-unencoded\n  (jws/sign k \"$.02\" {:detached? true :b64? false}))\n(jws/verify-detached k detached-unencoded \"$.02\")\n```\n\n`:detached? true` serializes compact JWS as `header..signature`.\n`:b64? false` sets RFC 7797 `b64:false` and marks `b64` critical. Attached\ncompact unencoded payloads cannot contain `.` because `.` is the compact JWS\ndelimiter; use detached unencoded JWS for arbitrary payload bytes.\n\n### Encryption (`jose.jwe`)\n\nFull algorithm matrix: `:rsa-oaep-256/384/512`, `:ecdh-es` and `:ecdh-1pu`\n(each with `+a128/192/256kw` variants), `:pbes2-hs256+a128kw`,\n`:pbes2-hs384+a192kw`, `:pbes2-hs512+a256kw`, `:a128/192/256kw`,\n`:a128/192/256gcmkw`, and `:dir`; encryption methods\n`:a128/192/256cbc-hs256/384/512`, `:a128/192/256gcm`, and `:xc20p`. The insecure\nRSA1_5 is deliberately not offered.\n\n```clojure\n(jwe/encrypt k \"payload\" {:alg :rsa-oaep-256 :enc :a256gcm})\n(jwe/decrypt k compact)                ; =\u003e {:payload .. :payload-bytes .. :header ..}\n```\n\n### JSON serialization\n\nCompact serialization remains the default for `sign` and `encrypt`. The JSON APIs\nsupport flattened serialization for one signature or recipient, and general\nserialization for multiple signatures or recipients:\n\n```clojure\n(def signed-json\n  (jws/sign-json k \"payload\" {:serialization :flattened}))\n(jws/verify-json k signed-json {:algs #{:rs256}})\n\n(def encrypted-json\n  (jwe/encrypt-json [(jwk/public-jwk recipient-1)\n                     (jwk/public-jwk recipient-2)]\n                    \"payload\"\n                    {:serialization :general\n                     :alg :rsa-oaep-256\n                     :enc :a256gcm}))\n(jwe/decrypt-json recipient-1 encrypted-json)\n```\n\n`sign-json` accepts per-signature protected and unprotected headers in general\nform. `encrypt-json` accepts shared protected and unprotected headers; general JWE\nplaces key-management data on each recipient.\n\n### JWTs (`jose.jwt`)\n\n```clojure\n(jwt/sign k {:sub \"alice\"} {:expires-in 3600 :now-iat? true})\n(jwt/verify k token {:iss \"me\" :aud \"you\" :clock-skew 60 :required [:sub]})\n;; validation failures throw ex-info: :expired :not-yet-valid\n;; :claim-mismatch :missing-claim :invalid-signature\n\n;; encrypted JWT\n(jwt/encrypt ek {:sub \"alice\"} {:alg :ecdh-es :enc :a256gcm})\n(jwt/decrypt ek token)\n\n;; nested: sign then encrypt, decrypt then verify\n(jwt/sign-then-encrypt sign-key enc-key {:sub \"alice\"})\n(jwt/decrypt-then-verify enc-key sign-key token {:iss \"me\"})\n```\n\n### Remote JWKS (`jose.jwks`)\n\n```clojure\n(require '[jose.jwks :as jwks])\n\n;; cached remote source (defaults shown are Nimbus defaults; all optional)\n(def src (jwks/remote-source \"https://www.googleapis.com/oauth2/v3/certs\"\n                             {:cache-ttl-ms 300000 :connect-timeout-ms 5000}))\n\n(jwks/get-keys src {:use :sig :kty :rsa})  ; matched keys\n(jwks/find-key src \"kid-123\")\n\n;; verify a token against the source: the key is picked by the token's kid and\n;; alg, and the verifier is chosen from the key type - so an HS256 forgery over\n;; an RSA public key is rejected, not confused for a valid MAC\n(jws/verify-with-jwks src compact)\n(jwt/verify-with-jwks src token {:iss \"https://accounts.google.com\"})\n```\n\n### Key rotation and public JWKS (`jose.keyring`)\n\n```clojure\n(require '[jose.keyring :as keyring])\n\n(def ring\n  (keyring/key-ring [signing-key encryption-key]\n                    {:active-signing-kid \"sig-1\"\n                     :active-encryption-kid \"enc-1\"}))\n\n(def token (keyring/sign ring \"payload\"))\n(keyring/verify ring token {:algs #{:rs256}})\n\n(def rotated (keyring/rotate ring :signing next-signing-key))\n(def pruned (keyring/prune rotated {:retention-seconds 86400}))\n\n(keyring/public-jwks rotated)       ; Nimbus JWKSet\n(keyring/public-jwks-json rotated)  ; publication-ready JWKS JSON\n```\n\nThe immutable key ring keeps active signing and encryption keys while retained\nkeys continue to verify or decrypt older messages. Published JWKS contain only\npublic key material: private parameters are stripped and symmetric keys are\nomitted.\n\n## Errors\n\nBad input never NPEs. API and parse failures normalize to `ex-info` with a\n`:jose/error` key and the original exception as the cause:\n\n| `:jose/error` | when |\n|---|---|\n| `:invalid-option` | unknown option keyword |\n| `:parse-failure` | malformed JWK / JWS / JWE / JWKS |\n| `:invalid-signature` / `:sign-failure` | JWS/JWT signature does not verify / signing failed |\n| `:encryption-failure` / `:decryption-failure` | JWE will not encrypt / decrypt with the given key |\n| `:expired` / `:not-yet-valid` | `exp` / `nbf` claim checks (with `:clock-skew`) |\n| `:claim-mismatch` / `:missing-claim` | `:aud` / `:iss` / `:required` checks |\n| `:key-not-found` / `:ambiguous-key` | JWKS key selection |\n| `:not-a-nested-jwt` | `decrypt-then-verify` on a JWE whose payload is not a JWT |\n| `:key-source-failure` | remote JWKS fetch failed |\n| `:key-length` | symmetric key wrong size for the algorithm |\n| `:missing-optional-dep` | optional Tink/BouncyCastle path without its runtime dependency |\n| `:invalid-url` | bad JWKS URL |\n\n## License\n\nCopyright © 2026 Savyasachi\n\nDistributed under the [Eclipse Public License 2.0](https://www.eclipse.org/legal/epl-2.0/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsavyasachi%2Fjose-clj","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsavyasachi%2Fjose-clj","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsavyasachi%2Fjose-clj/lists"}