diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# v1.2.0
+
+* Switch from `jwt` to `jose-jwt` + `crypton`
+    * Removes the `loadSigner` helper, use normal `crypton`/`crypton-x509`/`crypton-x509-store` API
+* Add support for GHC 9.8 + 9.10
+* Drop support for GHC < 9.6
+
 # v1.1.4
 
 * Fix a test failure due to GitHub changing URLs
diff --git a/github-rest.cabal b/github-rest.cabal
--- a/github-rest.cabal
+++ b/github-rest.cabal
@@ -1,11 +1,11 @@
 cabal-version: >= 1.10
 
--- This file has been generated from package.yaml by hpack version 0.35.2.
+-- This file has been generated from package.yaml by hpack version 0.36.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           github-rest
-version:        1.1.4
+version:        1.2.0
 synopsis:       Query the GitHub REST API programmatically
 description:    Query the GitHub REST API programmatically, which can provide a more
                 flexible and clear interface than if all of the endpoints and their types
@@ -45,16 +45,17 @@
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances -Wunused-packages
   build-depends:
       aeson <3
-    , base >=4.15 && <5
-    , bytestring <0.12
+    , base <5
+    , bytestring <0.13
+    , crypton <1.1
     , http-client >=0.5.13 && <0.8
     , http-client-tls <0.4
     , http-types >=0.11 && <0.13
-    , jwt >=0.9 && <0.12
+    , jose-jwt >=0.10.0 && <0.11
     , mtl <2.4
     , scientific <0.4
-    , text <2.1
-    , time <1.13
+    , text <2.2
+    , time <1.15
     , transformers <0.7
     , unliftio <0.3
     , unliftio-core <0.3
@@ -64,6 +65,7 @@
   type: exitcode-stdio-1.0
   main-is: Main.hs
   other-modules:
+      Auth
       Endpoint
       Helpers
       MockQuery
@@ -75,9 +77,10 @@
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances -Wunused-packages
   build-depends:
       aeson >=1.5.6.0
-    , aeson-qq
+    , aeson-qq >=0.8.4
     , base
     , bytestring
+    , crypton
     , github-rest
     , http-types
     , mtl
diff --git a/src/GitHub/REST/Auth.hs b/src/GitHub/REST/Auth.hs
--- a/src/GitHub/REST/Auth.hs
+++ b/src/GitHub/REST/Auth.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 
@@ -16,27 +16,21 @@
 
   -- * Helpers for using JWT tokens with the GitHub API
   getJWTToken,
-  loadSigner,
 ) where
 
+import qualified Crypto.PubKey.RSA as Crypto
+import Data.Aeson ((.=))
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Text as Aeson
 import Data.ByteString (ByteString)
-import qualified Data.ByteString as ByteString
-
-#if !MIN_VERSION_base(4,11,0)
-import Data.Monoid ((<>))
-#endif
-import Data.Text (Text)
-import qualified Data.Text as Text
 import qualified Data.Text.Encoding as Text
-import Data.Time (addUTCTime, getCurrentTime)
+import qualified Data.Text.Lazy as TextL
+import Data.Time (getCurrentTime)
 import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
-import qualified Web.JWT as JWT
-
-#if MIN_VERSION_jwt(0,11,0)
-type EncodeSigner = JWT.EncodeSigner
-#else
-type EncodeSigner = JWT.Signer
-#endif
+import qualified Jose.Jwa as Jose
+import qualified Jose.Jws as Jose
+import qualified Jose.Jwt as Jose
+import UnliftIO.Exception (Exception, throwIO)
 
 -- | The token to use to authenticate with GitHub.
 data Token
@@ -55,37 +49,26 @@
 type AppId = Int
 
 -- | Create a JWT token that expires in 10 minutes.
-getJWTToken :: EncodeSigner -> AppId -> IO Token
-getJWTToken signer appId = mkToken <$> getNow
-  where
-    mkToken now =
-      let claims =
-            mempty
-              { JWT.iat = JWT.numericDate $ utcTimeToPOSIXSeconds now
-              , JWT.exp = JWT.numericDate $ utcTimeToPOSIXSeconds now + (10 * 60)
-              , JWT.iss = JWT.stringOrURI $ Text.pack $ show appId
-              }
-       in BearerToken . Text.encodeUtf8 $ signToken signer claims
-    -- lose a second in the case of rounding
-    -- https://github.community/t5/GitHub-API-Development-and/quot-Expiration-time-claim-exp-is-too-far-in-the-future-quot/m-p/20457/highlight/true#M1127
-    getNow = addUTCTime (-1) <$> getCurrentTime
-
-signToken :: EncodeSigner -> JWT.JWTClaimsSet -> Text
-#if MIN_VERSION_jwt(0,10,0)
-signToken = flip JWT.encodeSigned mempty
-#else
-signToken = JWT.encodeSigned
-#endif
+getJWTToken :: Crypto.PrivateKey -> AppId -> IO Token
+getJWTToken privKey appId = do
+  -- use floor to ensure expiration doesn't go past 10 minutes
+  -- https://github.com/orgs/community/discussions/24635#discussioncomment-3244803
+  now <- floor . utcTimeToPOSIXSeconds <$> getCurrentTime
 
--- | Load a RSA private key as a Signer from the given file path.
-loadSigner :: FilePath -> IO EncodeSigner
-loadSigner file = maybe badSigner return . readSigner =<< ByteString.readFile file
+  BearerToken . Jose.unJwt <$> signToken (mkClaims now)
   where
-    badSigner = fail $ "Not a valid RSA private key file: " ++ file
-    readSigner = fmap toEncodeRSAPrivateKey . JWT.readRsaSecret
+    mkClaims now =
+      Text.encodeUtf8 . TextL.toStrict . Aeson.encodeToLazyText $
+        Aeson.object
+          [ "iat" .= (now :: Integer)
+          , "exp" .= (now + 10 * 60)
+          , "iss" .= show appId
+          ]
+    signToken claims =
+      Jose.rsaEncode Jose.RS256 privKey claims >>= \case
+        Right jwt -> pure jwt
+        Left e -> throwIO $ JwtError e
 
-#if MIN_VERSION_jwt(0,11,0)
-    toEncodeRSAPrivateKey = JWT.EncodeRSAPrivateKey
-#else
-    toEncodeRSAPrivateKey = JWT.RSAPrivateKey
-#endif
+-- https://github.com/tekul/jose-jwt/issues/30
+data JwtError = JwtError Jose.JwtError
+  deriving (Show, Exception)
diff --git a/test/Auth.hs b/test/Auth.hs
new file mode 100644
--- /dev/null
+++ b/test/Auth.hs
@@ -0,0 +1,15 @@
+module Auth (tests) where
+
+import qualified Crypto.PubKey.RSA as Crypto
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit
+
+import GitHub.REST.Auth
+
+tests :: [TestTree]
+tests =
+  [ testCase "getJWTToken works" $ do
+      (_, privKey) <- Crypto.generate 256 3
+      _ <- getJWTToken privKey 123
+      pure ()
+  ]
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,5 +1,6 @@
 import Test.Tasty (defaultMain, testGroup)
 
+import qualified Auth
 import qualified Endpoint
 import qualified Helpers
 import qualified MockQuery
@@ -15,5 +16,6 @@
       , testGroup "GitHub.REST.Endpoint" Endpoint.tests
       , testGroup "GitHub.REST.Monad.Class" MockQuery.tests
       , testGroup "GitHub.REST.PageLinks" PageLinks.tests
+      , testGroup "GitHub.REST.Auth" Auth.tests
       , testGroup "GitHub.REST (End-to-End)" Query.tests
       ]
