diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,21 @@
 Changelog
 ==========
 
+## Version 1.7.0 -- Unreleased
+
+* Fix data type `EventMessage` to not export partial fields (#179)
+* Bump up min bound on time dep and fix deprecation warnings (#181)
+* Add `dev` flag to cabal for building with warnings as errors (#182)
+* Fix AWS region map (#185)
+* Fix XML generator tests (#187)
+* Add support for STS Assume Role API (#188)
+
+### Breaking changes in 1.7.0
+
+* `Credentials` type has been removed. Use `CredentialValue` instead.
+* `Provider` type has been replaced with `CredentialLoader`.
+* `EventMessage` data type is updated.
+
 ## Version 1.6.0
 
 * HLint fixes - some types were changed to newtype (#173)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
-# MinIO Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
-
-The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
 
-## Minimum Requirements
+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
 
-- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
 
 ## Installation
 
@@ -12,20 +10,35 @@
 
 Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
 
-### Try it out directly with `ghci`
+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
 
+#### For a cabal based environment
+
+Download the library source and change to the extracted directory:
+
+``` sh
+$ cabal get minio-hs
+$ cd minio-hs-1.6.0/ # directory name could be different
+```
+
+Then load the `ghci` REPL environment with the library and browse the available APIs:
+
+``` sh
+$ cabal repl
+ghci> :browse Network.Minio
+```
+
+#### For a stack based environment
+
 From your home folder or any non-haskell project directory, just run:
 
 ```sh
-
 stack install minio-hs
-
 ```
 
 Then start an interpreter session and browse the available APIs with:
 
 ```sh
-
 $ stack ghci
 > :browse Network.Minio
 ```
@@ -134,44 +147,52 @@
 
 ### Development
 
-To setup:
+#### Download the source
 
 ```sh
-git clone https://github.com/minio/minio-hs.git
-
-cd minio-hs/
+$ git clone https://github.com/minio/minio-hs.git
+$ cd minio-hs/
+``` 
 
-stack install
-```
+#### Build the package:
 
-Tests can be run with:
+With `cabal`:
 
 ```sh
+$ # Configure cabal for development enabling all optional flags defined by the package.
+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
+$ cabal build
+```
 
-stack test
+With `stack`:
 
+``` sh
+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
 ```
+#### Running tests:
 
-A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
 
-To run the live server tests, set a build flag as shown below:
+With `cabal`:
 
 ```sh
-
-stack test --flag minio-hs:live-test
-
-# OR against a local MinIO server with:
+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
+$ cabal test
+```
 
-MINIO_LOCAL=1 stack test --flag minio-hs:live-test
+With `stack`:
 
+``` sh
+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
+stack test --flag minio-hs:live-test --flag minio-hs:dev
 ```
 
-The configured CI system always runs both test-suites for every change.
+This will run all the test suites.
 
-Documentation can be locally built with:
+#### Building documentation:
 
 ```sh
-
-stack haddock
-
+$ cabal haddock
+$ # OR
+$ stack haddock
 ```
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,19 +0,0 @@
---
--- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     http://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
---
-
-import Distribution.Simple
-
-main = defaultMain
diff --git a/examples/AssumeRole.hs b/examples/AssumeRole.hs
new file mode 100644
--- /dev/null
+++ b/examples/AssumeRole.hs
@@ -0,0 +1,47 @@
+--
+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Monad.IO.Class (liftIO)
+import Network.Minio
+import Prelude
+
+main :: IO ()
+main = do
+  -- Use play credentials for example.
+  let assumeRole =
+        STSAssumeRole
+          ( CredentialValue
+              "Q3AM3UQ867SPQQA43P2F"
+              "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
+              Nothing
+          )
+          $ defaultSTSAssumeRoleOptions
+            { saroLocation = Just "us-east-1",
+              saroEndpoint = Just "https://play.min.io:9000"
+            }
+
+  -- Retrieve temporary credentials and print them.
+  cv <- requestSTSCredential assumeRole
+  print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
+  print $ "Expiry" ++ show (snd cv)
+
+  -- Configure 'ConnectInfo' to request temporary credentials on demand.
+  ci <- setSTSCredential assumeRole "https://play.min.io"
+  res <- runMinio ci $ do
+    buckets <- listBuckets
+    liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
+  print res
diff --git a/minio-hs.cabal b/minio-hs.cabal
--- a/minio-hs.cabal
+++ b/minio-hs.cabal
@@ -1,6 +1,6 @@
-cabal-version:       2.2
+cabal-version:       2.4
 name:                minio-hs
-version:             1.6.0
+version:             1.7.0
 synopsis:            A MinIO Haskell Library for Amazon S3 compatible cloud
                      storage.
 description:         The MinIO Haskell client library provides simple APIs to
@@ -14,21 +14,29 @@
 category:            Network, AWS, Object Storage
 build-type:          Simple
 stability:           Experimental
-extra-source-files:
+extra-doc-files:
                    CHANGELOG.md
                    CONTRIBUTING.md
                    docs/API.md
-                   examples/*.hs
                    README.md
+extra-source-files:
+                   examples/*.hs
                    stack.yaml
-tested-with:         GHC == 8.8.4
+tested-with:         GHC == 8.6.5
+                   , GHC == 8.8.4
                    , GHC == 8.10.7
                    , GHC == 9.0.2
+                   , GHC == 9.2.7
+                   , GHC == 9.4.5
 
 source-repository head
   type:                git
   location:            https://github.com/minio/minio-hs.git
 
+Flag dev
+  Description: Build package in development mode
+  Default: False
+  Manual: True
 
 common base-settings
   ghc-options:         -Wall
@@ -41,16 +49,20 @@
     ghc-options:       -Wredundant-constraints
   if impl(ghc >= 8.2)
     ghc-options:       -fhide-source-paths
-
-  -- Add this when we have time. Fixing partial-fields requires major version
-  -- bump at this time.
-  -- if impl(ghc >= 8.4)
-  --   ghc-options:       -Wpartial-fields
-  --                      -Wmissing-export-lists
-
+  if impl(ghc >= 8.4)
+    ghc-options:       -Wpartial-fields
+  --                     -Wmissing-export-lists
   if impl(ghc >= 8.8)
     ghc-options:       -Wmissing-deriving-strategies
                        -Werror=missing-deriving-strategies
+  -- if impl(ghc >= 8.10)
+  --   ghc-options:       -Wunused-packages -- disabled due to bug related to mixin config
+  if impl(ghc >= 9.0)
+    ghc-options:       -Winvalid-haddock
+  if impl(ghc >= 9.2)
+    ghc-options:       -Wredundant-bang-patterns
+  if flag(dev)
+    ghc-options:       -Werror
 
   default-language:    Haskell2010
 
@@ -65,9 +77,7 @@
                      , RankNTypes
                      , ScopedTypeVariables
                      , TupleSections
-                     , TypeFamilies
 
-
   other-modules:       Lib.Prelude
                      , Network.Minio.API
                      , Network.Minio.APICommon
@@ -85,7 +95,11 @@
                      , Network.Minio.Utils
                      , Network.Minio.XmlGenerator
                      , Network.Minio.XmlParser
+                     , Network.Minio.XmlCommon
                      , Network.Minio.JsonParser
+                     , Network.Minio.Credentials.Types
+                     , Network.Minio.Credentials.AssumeRole
+                     , Network.Minio.Credentials
 
   mixins:              base hiding (Prelude)
                      , relude (Relude as Prelude)
@@ -105,7 +119,6 @@
                      , cryptonite-conduit >= 0.2
                      , digest >= 0.0.1
                      , directory
-                     , exceptions
                      , filepath >= 1.4
                      , http-client >= 0.5
                      , http-client-tls
@@ -114,11 +127,11 @@
                      , ini
                      , memory >= 0.14
                      , network-uri
-                     , raw-strings-qq >= 1
                      , resourcet >= 1.2
                      , retry
                      , text >= 1.2
-                     , time >= 1.8
+                     , time >= 1.9
+                     , time-units ^>= 1.0.0
                      , transformers >= 0.5
                      , unliftio >= 0.2 && < 0.3
                      , unliftio-core >= 0.2 && < 0.3
@@ -152,7 +165,9 @@
                      , Network.Minio.Utils.Test
                      , Network.Minio.XmlGenerator.Test
                      , Network.Minio.XmlParser.Test
+                     , Network.Minio.Credentials
   build-depends:       minio-hs
+                     , raw-strings-qq
                      , tasty
                      , tasty-hunit
                      , tasty-quickcheck
@@ -167,6 +182,7 @@
   hs-source-dirs:      test, src
   main-is:             Spec.hs
   build-depends:       minio-hs
+                     , raw-strings-qq
                      , QuickCheck
                      , tasty
                      , tasty-hunit
@@ -183,6 +199,7 @@
                      , Network.Minio.Utils.Test
                      , Network.Minio.XmlGenerator.Test
                      , Network.Minio.XmlParser.Test
+                     , Network.Minio.Credentials
 
 Flag examples
   Description: Build the examples
@@ -328,3 +345,8 @@
   import:              examples-settings
   scope:               private
   main-is:             SetConfig.hs
+
+executable AssumeRole
+  import:              examples-settings
+  scope:               private
+  main-is:             AssumeRole.hs
diff --git a/src/Lib/Prelude.hs b/src/Lib/Prelude.hs
--- a/src/Lib/Prelude.hs
+++ b/src/Lib/Prelude.hs
@@ -42,7 +42,7 @@
 both :: (a -> b) -> (a, a) -> (b, b)
 both f (a, b) = (f a, f b)
 
-showBS :: Show a => a -> ByteString
+showBS :: (Show a) => a -> ByteString
 showBS a = encodeUtf8 (show a :: Text)
 
 toStrictBS :: LByteString -> ByteString
diff --git a/src/Network/Minio.hs b/src/Network/Minio.hs
--- a/src/Network/Minio.hs
+++ b/src/Network/Minio.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
 
 -- |
 -- Module:      Network.Minio
--- Copyright:   (c) 2017-2019 MinIO Dev Team
+-- Copyright:   (c) 2017-2023 MinIO Dev Team
 -- License:     Apache 2.0
 -- Maintainer:  MinIO Dev Team <dev@min.io>
 --
@@ -24,13 +24,17 @@
 -- storage servers like MinIO.
 module Network.Minio
   ( -- * Credentials
-    Credentials (..),
+    CredentialValue (..),
+    credentialValueText,
+    AccessKey (..),
+    SecretKey (..),
+    SessionToken (..),
 
-    -- ** Credential providers
+    -- ** Credential Loaders
 
-    -- | Run actions that retrieve 'Credentials' from the environment or
+    -- | Run actions that retrieve 'CredentialValue's from the environment or
     -- files or other custom sources.
-    Provider,
+    CredentialLoader,
     fromAWSConfigFile,
     fromAWSEnv,
     fromMinioEnv,
@@ -54,6 +58,15 @@
     awsCI,
     gcsCI,
 
+    -- ** STS Credential types
+    STSAssumeRole (..),
+    STSAssumeRoleOptions (..),
+    defaultSTSAssumeRoleOptions,
+    requestSTSCredential,
+    setSTSCredential,
+    ExpiryTime (..),
+    STSCredentialProvider,
+
     -- * Minio Monad
 
     ----------------
@@ -225,14 +238,15 @@
 import qualified Data.Conduit as C
 import qualified Data.Conduit.Binary as CB
 import qualified Data.Conduit.Combinators as CC
+import Network.Minio.API
 import Network.Minio.CopyObject
+import Network.Minio.Credentials
 import Network.Minio.Data
 import Network.Minio.Errors
 import Network.Minio.ListOps
 import Network.Minio.PutObject
 import Network.Minio.S3API
 import Network.Minio.SelectAPI
-import Network.Minio.Utils
 
 -- | Lists buckets.
 listBuckets :: Minio [BucketInfo]
diff --git a/src/Network/Minio/API.hs b/src/Network/Minio/API.hs
--- a/src/Network/Minio/API.hs
+++ b/src/Network/Minio/API.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
     checkBucketNameValidity,
     isValidObjectName,
     checkObjectNameValidity,
+    requestSTSCredential,
   )
 where
 
@@ -41,12 +42,15 @@
 import qualified Data.Text as T
 import qualified Data.Time.Clock as Time
 import Lib.Prelude
+import Network.HTTP.Client (defaultManagerSettings)
 import qualified Network.HTTP.Client as NClient
 import Network.HTTP.Conduit (Response)
 import qualified Network.HTTP.Conduit as NC
+import Network.HTTP.Types (simpleQueryToQuery)
 import qualified Network.HTTP.Types as HT
 import Network.HTTP.Types.Header (hHost)
 import Network.Minio.APICommon
+import Network.Minio.Credentials
 import Network.Minio.Data
 import Network.Minio.Errors
 import Network.Minio.Sign.V4
@@ -143,6 +147,20 @@
           else return pathStyle
         )
 
+-- | requestSTSCredential requests temporary credentials using the Security Token
+-- Service API. The returned credential will include a populated 'SessionToken'
+-- and an 'ExpiryTime'.
+requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
+requestSTSCredential p = do
+  endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
+  let endPt = NC.parseRequest_ $ toString endpoint
+      settings
+        | NC.secure endPt = NC.tlsManagerSettings
+        | otherwise = defaultManagerSettings
+
+  mgr <- NC.newManager settings
+  liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
+
 buildRequest :: S3ReqInfo -> Minio NC.Request
 buildRequest ri = do
   maybe (return ()) checkBucketNameValidity $ riBucket ri
@@ -173,10 +191,15 @@
 
   timeStamp <- liftIO Time.getCurrentTime
 
+  mgr <- asks mcConnManager
+  cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
+
   let sp =
         SignParams
-          (connectAccessKey ci')
-          (connectSecretKey ci')
+          (coerce $ cvAccessKey cv)
+          (coerce $ cvSecretKey cv)
+          (coerce $ cvSessionToken cv)
+          ServiceS3
           timeStamp
           (riRegion ri')
           (riPresignExpirySecs ri')
@@ -198,8 +221,8 @@
       | isJust (riPresignExpirySecs ri') ->
           -- case 0 from above.
           do
-            let signPairs = signV4 sp baseRequest
-                qpToAdd = (fmap . fmap) Just signPairs
+            let signPairs = signV4QueryParams sp baseRequest
+                qpToAdd = simpleQueryToQuery signPairs
                 existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
                 updatedQueryParams = existingQueryParams ++ qpToAdd
             return $ NClient.setQueryString updatedQueryParams baseRequest
@@ -229,8 +252,7 @@
             return $
               baseRequest
                 { NC.requestHeaders =
-                    NC.requestHeaders baseRequest
-                      ++ mkHeaderFromPairs signHeaders,
+                    NC.requestHeaders baseRequest ++ signHeaders,
                   NC.requestBody = getRequestBody (riPayload ri')
                 }
 
@@ -315,7 +337,7 @@
     isIPCheck = and labelAsNums && length labelAsNums == 4
 
 -- Throws exception iff bucket name is invalid according to AWS rules.
-checkBucketNameValidity :: MonadIO m => Bucket -> m ()
+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
 checkBucketNameValidity bucket =
   unless (isValidBucketName bucket) $
     throwIO $
@@ -325,7 +347,7 @@
 isValidObjectName object =
   T.length object > 0 && B.length (encodeUtf8 object) <= 1024
 
-checkObjectNameValidity :: MonadIO m => Object -> m ()
+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
 checkObjectNameValidity object =
   unless (isValidObjectName object) $
     throwIO $
diff --git a/src/Network/Minio/AdminAPI.hs b/src/Network/Minio/AdminAPI.hs
--- a/src/Network/Minio/AdminAPI.hs
+++ b/src/Network/Minio/AdminAPI.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -80,6 +80,7 @@
 import qualified Network.HTTP.Types as HT
 import Network.HTTP.Types.Header (hHost)
 import Network.Minio.APICommon
+import Network.Minio.Credentials
 import Network.Minio.Data
 import Network.Minio.Errors
 import Network.Minio.Sign.V4
@@ -95,9 +96,12 @@
 instance FromJSON DriveInfo where
   parseJSON = withObject "DriveInfo" $ \v ->
     DriveInfo
-      <$> v .: "uuid"
-      <*> v .: "endpoint"
-      <*> v .: "state"
+      <$> v
+        .: "uuid"
+      <*> v
+        .: "endpoint"
+      <*> v
+        .: "state"
 
 data StorageClass = StorageClass
   { scParity :: Int,
@@ -120,12 +124,16 @@
     offlineDisks <- v .: "OfflineDisks"
     stdClass <-
       StorageClass
-        <$> v .: "StandardSCData"
-        <*> v .: "StandardSCParity"
+        <$> v
+          .: "StandardSCData"
+        <*> v
+          .: "StandardSCParity"
     rrClass <-
       StorageClass
-        <$> v .: "RRSCData"
-        <*> v .: "RRSCParity"
+        <$> v
+          .: "RRSCData"
+        <*> v
+          .: "RRSCParity"
     sets <- v .: "Sets"
     return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
 
@@ -151,8 +159,10 @@
 instance FromJSON ConnStats where
   parseJSON = withObject "ConnStats" $ \v ->
     ConnStats
-      <$> v .: "transferred"
-      <*> v .: "received"
+      <$> v
+        .: "transferred"
+      <*> v
+        .: "received"
 
 data ServerProps = ServerProps
   { spUptime :: NominalDiffTime,
@@ -182,8 +192,10 @@
 instance FromJSON StorageInfo where
   parseJSON = withObject "StorageInfo" $ \v ->
     StorageInfo
-      <$> v .: "Used"
-      <*> v .: "Backend"
+      <$> v
+        .: "Used"
+      <*> v
+        .: "Backend"
 
 data CountNAvgTime = CountNAvgTime
   { caCount :: Int64,
@@ -194,8 +206,10 @@
 instance FromJSON CountNAvgTime where
   parseJSON = withObject "CountNAvgTime" $ \v ->
     CountNAvgTime
-      <$> v .: "count"
-      <*> v .: "avgDuration"
+      <$> v
+        .: "count"
+      <*> v
+        .: "avgDuration"
 
 data HttpStats = HttpStats
   { hsTotalHeads :: CountNAvgTime,
@@ -214,16 +228,26 @@
 instance FromJSON HttpStats where
   parseJSON = withObject "HttpStats" $ \v ->
     HttpStats
-      <$> v .: "totalHEADs"
-      <*> v .: "successHEADs"
-      <*> v .: "totalGETs"
-      <*> v .: "successGETs"
-      <*> v .: "totalPUTs"
-      <*> v .: "successPUTs"
-      <*> v .: "totalPOSTs"
-      <*> v .: "successPOSTs"
-      <*> v .: "totalDELETEs"
-      <*> v .: "successDELETEs"
+      <$> v
+        .: "totalHEADs"
+      <*> v
+        .: "successHEADs"
+      <*> v
+        .: "totalGETs"
+      <*> v
+        .: "successGETs"
+      <*> v
+        .: "totalPUTs"
+      <*> v
+        .: "successPUTs"
+      <*> v
+        .: "totalPOSTs"
+      <*> v
+        .: "successPOSTs"
+      <*> v
+        .: "totalDELETEs"
+      <*> v
+        .: "successDELETEs"
 
 data SIData = SIData
   { sdStorage :: StorageInfo,
@@ -236,10 +260,14 @@
 instance FromJSON SIData where
   parseJSON = withObject "SIData" $ \v ->
     SIData
-      <$> v .: "storage"
-      <*> v .: "network"
-      <*> v .: "http"
-      <*> v .: "server"
+      <$> v
+        .: "storage"
+      <*> v
+        .: "network"
+      <*> v
+        .: "http"
+      <*> v
+        .: "server"
 
 data ServerInfo = ServerInfo
   { siError :: Text,
@@ -251,9 +279,12 @@
 instance FromJSON ServerInfo where
   parseJSON = withObject "ServerInfo" $ \v ->
     ServerInfo
-      <$> v .: "error"
-      <*> v .: "addr"
-      <*> v .: "data"
+      <$> v
+        .: "error"
+      <*> v
+        .: "addr"
+      <*> v
+        .: "data"
 
 data ServerVersion = ServerVersion
   { svVersion :: Text,
@@ -264,8 +295,10 @@
 instance FromJSON ServerVersion where
   parseJSON = withObject "ServerVersion" $ \v ->
     ServerVersion
-      <$> v .: "version"
-      <*> v .: "commitID"
+      <$> v
+        .: "version"
+      <*> v
+        .: "commitID"
 
 data ServiceStatus = ServiceStatus
   { ssVersion :: ServerVersion,
@@ -306,9 +339,12 @@
 instance FromJSON HealStartResp where
   parseJSON = withObject "HealStartResp" $ \v ->
     HealStartResp
-      <$> v .: "clientToken"
-      <*> v .: "clientAddress"
-      <*> v .: "startTime"
+      <$> v
+        .: "clientToken"
+      <*> v
+        .: "clientAddress"
+      <*> v
+        .: "startTime"
 
 data HealOpts = HealOpts
   { hoRecursive :: Bool,
@@ -325,8 +361,10 @@
 instance FromJSON HealOpts where
   parseJSON = withObject "HealOpts" $ \v ->
     HealOpts
-      <$> v .: "recursive"
-      <*> v .: "dryRun"
+      <$> v
+        .: "recursive"
+      <*> v
+        .: "dryRun"
 
 data HealItemType
   = HealItemMetadata
@@ -353,9 +391,12 @@
 instance FromJSON NodeSummary where
   parseJSON = withObject "NodeSummary" $ \v ->
     NodeSummary
-      <$> v .: "name"
-      <*> v .: "errSet"
-      <*> v .: "errMsg"
+      <$> v
+        .: "name"
+      <*> v
+        .: "errSet"
+      <*> v
+        .: "errMsg"
 
 data SetConfigResult = SetConfigResult
   { scrStatus :: Bool,
@@ -366,8 +407,10 @@
 instance FromJSON SetConfigResult where
   parseJSON = withObject "SetConfigResult" $ \v ->
     SetConfigResult
-      <$> v .: "status"
-      <*> v .: "nodeResults"
+      <$> v
+        .: "status"
+      <*> v
+        .: "nodeResults"
 
 data HealResultItem = HealResultItem
   { hriResultIdx :: Int,
@@ -388,16 +431,26 @@
 instance FromJSON HealResultItem where
   parseJSON = withObject "HealResultItem" $ \v ->
     HealResultItem
-      <$> v .: "resultId"
-      <*> v .: "type"
-      <*> v .: "bucket"
-      <*> v .: "object"
-      <*> v .: "detail"
-      <*> v .:? "parityBlocks"
-      <*> v .:? "dataBlocks"
-      <*> v .: "diskCount"
-      <*> v .: "setCount"
-      <*> v .: "objectSize"
+      <$> v
+        .: "resultId"
+      <*> v
+        .: "type"
+      <*> v
+        .: "bucket"
+      <*> v
+        .: "object"
+      <*> v
+        .: "detail"
+      <*> v
+        .:? "parityBlocks"
+      <*> v
+        .:? "dataBlocks"
+      <*> v
+        .: "diskCount"
+      <*> v
+        .: "setCount"
+      <*> v
+        .: "objectSize"
       <*> ( do
               before <- v .: "before"
               before .: "drives"
@@ -420,12 +473,18 @@
 instance FromJSON HealStatus where
   parseJSON = withObject "HealStatus" $ \v ->
     HealStatus
-      <$> v .: "Summary"
-      <*> v .: "StartTime"
-      <*> v .: "Settings"
-      <*> v .: "NumDisks"
-      <*> v .:? "Detail"
-      <*> v .: "Items"
+      <$> v
+        .: "Summary"
+      <*> v
+        .: "StartTime"
+      <*> v
+        .: "Settings"
+      <*> v
+        .: "NumDisks"
+      <*> v
+        .:? "Detail"
+      <*> v
+        .: "Items"
 
 healPath :: Maybe Bucket -> Maybe Text -> ByteString
 healPath bucket prefix = do
@@ -607,6 +666,9 @@
 
   timeStamp <- liftIO getCurrentTime
 
+  mgr <- asks mcConnManager
+  cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
+
   let hostHeader = (hHost, getHostAddr ci)
       newAreq =
         areq
@@ -619,8 +681,10 @@
       signReq = toRequest ci newAreq
       sp =
         SignParams
-          (connectAccessKey ci)
-          (connectSecretKey ci)
+          (coerce $ cvAccessKey cv)
+          (coerce $ cvSecretKey cv)
+          (coerce $ cvSessionToken cv)
+          ServiceS3
           timeStamp
           Nothing
           Nothing
@@ -630,7 +694,7 @@
   -- Update signReq with Authorization header containing v4 signature
   return
     signReq
-      { NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
+      { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
       }
   where
     toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
diff --git a/src/Network/Minio/Credentials.hs b/src/Network/Minio/Credentials.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Minio/Credentials.hs
@@ -0,0 +1,77 @@
+--
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+module Network.Minio.Credentials
+  ( CredentialValue (..),
+    credentialValueText,
+    STSCredentialProvider (..),
+    AccessKey (..),
+    SecretKey (..),
+    SessionToken (..),
+    ExpiryTime (..),
+    STSCredentialStore,
+    initSTSCredential,
+    getSTSCredential,
+    Creds (..),
+    getCredential,
+    Endpoint,
+
+    -- * STS Assume Role
+    defaultSTSAssumeRoleOptions,
+    STSAssumeRole (..),
+    STSAssumeRoleOptions (..),
+  )
+where
+
+import Data.Time (diffUTCTime, getCurrentTime)
+import qualified Network.HTTP.Client as NC
+import Network.Minio.Credentials.AssumeRole
+import Network.Minio.Credentials.Types
+import qualified UnliftIO.MVar as M
+
+data STSCredentialStore = STSCredentialStore
+  { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
+    refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
+  }
+
+initSTSCredential :: (STSCredentialProvider p) => p -> IO STSCredentialStore
+initSTSCredential p = do
+  let action = retrieveSTSCredentials p
+  -- start with dummy credential, so that refresh happens for first request.
+  now <- getCurrentTime
+  mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
+  return $
+    STSCredentialStore
+      { cachedCredentials = mvar,
+        refreshAction = action
+      }
+
+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
+  now <- getCurrentTime
+  if diffUTCTime now (coerce expiry) > 0
+    then do
+      res <- refreshAction store ep mgr
+      return (res, (fst res, True))
+    else return (cc, (v, False))
+
+data Creds
+  = CredsStatic CredentialValue
+  | CredsSTS STSCredentialStore
+
+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
+getCredential (CredsStatic v) _ _ = return v
+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
diff --git a/src/Network/Minio/Credentials/AssumeRole.hs b/src/Network/Minio/Credentials/AssumeRole.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Minio/Credentials/AssumeRole.hs
@@ -0,0 +1,266 @@
+--
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+module Network.Minio.Credentials.AssumeRole where
+
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString.Lazy as LB
+import qualified Data.Text as T
+import qualified Data.Time as Time
+import Data.Time.Units (Second)
+import Lib.Prelude (UTCTime, throwIO)
+import Network.HTTP.Client (RequestBody (RequestBodyBS))
+import qualified Network.HTTP.Client as NC
+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
+import Network.HTTP.Types.Header (hHost)
+import Network.Minio.Credentials.Types
+import Network.Minio.Data.Crypto (hashSHA256)
+import Network.Minio.Errors (MErrV (..))
+import Network.Minio.Sign.V4
+import Network.Minio.Utils (getHostHeader, httpLbs)
+import Network.Minio.XmlCommon
+import Text.XML.Cursor hiding (bool)
+
+stsVersion :: ByteString
+stsVersion = "2011-06-15"
+
+defaultDurationSeconds :: Second
+defaultDurationSeconds = 3600
+
+-- | Assume Role API argument.
+--
+-- @since 1.7.0
+data STSAssumeRole = STSAssumeRole
+  { -- | Credentials to use in the AssumeRole STS API.
+    sarCredentials :: CredentialValue,
+    -- | Optional settings.
+    sarOptions :: STSAssumeRoleOptions
+  }
+
+-- | Options for STS Assume Role API.
+data STSAssumeRoleOptions = STSAssumeRoleOptions
+  { -- | STS endpoint to which the request will be made. For MinIO, this is the
+    -- same as the server endpoint. For AWS, this has to be the Security Token
+    -- Service endpoint. If using with 'setSTSCredential', this option can be
+    -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
+    saroEndpoint :: Maybe Text,
+    -- | Desired validity for the generated credentials.
+    saroDurationSeconds :: Maybe Second,
+    -- | IAM policy to apply for the generated credentials.
+    saroPolicyJSON :: Maybe ByteString,
+    -- | Location is usually required for AWS.
+    saroLocation :: Maybe Text,
+    saroRoleARN :: Maybe Text,
+    saroRoleSessionName :: Maybe Text
+  }
+
+-- | Default STS Assume Role options - all options are Nothing, except for
+-- duration which is set to 1 hour.
+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
+defaultSTSAssumeRoleOptions =
+  STSAssumeRoleOptions
+    { saroEndpoint = Nothing,
+      saroDurationSeconds = Just 3600,
+      saroPolicyJSON = Nothing,
+      saroLocation = Nothing,
+      saroRoleARN = Nothing,
+      saroRoleSessionName = Nothing
+    }
+
+data AssumeRoleCredentials = AssumeRoleCredentials
+  { arcCredentials :: CredentialValue,
+    arcExpiration :: UTCTime
+  }
+  deriving stock (Show, Eq)
+
+data AssumeRoleResult = AssumeRoleResult
+  { arrSourceIdentity :: Text,
+    arrAssumedRoleArn :: Text,
+    arrAssumedRoleId :: Text,
+    arrRoleCredentials :: AssumeRoleCredentials
+  }
+  deriving stock (Show, Eq)
+
+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
+--
+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
+--   <AssumeRoleResult>
+--   <SourceIdentity>Alice</SourceIdentity>
+--     <AssumedRoleUser>
+--       <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
+--       <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
+--     </AssumedRoleUser>
+--     <Credentials>
+--       <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
+--       <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
+--       <SessionToken>
+--        AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
+--        LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
+--        QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
+--        9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
+--        +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
+--       </SessionToken>
+--       <Expiration>2019-11-09T13:34:41Z</Expiration>
+--     </Credentials>
+--     <PackedPolicySize>6</PackedPolicySize>
+--   </AssumeRoleResult>
+--   <ResponseMetadata>
+--     <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
+--   </ResponseMetadata>
+-- </AssumeRoleResponse>
+parseSTSAssumeRoleResult :: (MonadIO m) => ByteString -> Text -> m AssumeRoleResult
+parseSTSAssumeRoleResult xmldata namespace = do
+  r <- parseRoot $ LB.fromStrict xmldata
+  let s3Elem' = s3Elem namespace
+      sourceIdentity =
+        T.concat $
+          r
+            $/ s3Elem' "AssumeRoleResult"
+            &/ s3Elem' "SourceIdentity"
+            &/ content
+      roleArn =
+        T.concat $
+          r
+            $/ s3Elem' "AssumeRoleResult"
+            &/ s3Elem' "AssumedRoleUser"
+            &/ s3Elem' "Arn"
+            &/ content
+      roleId =
+        T.concat $
+          r
+            $/ s3Elem' "AssumeRoleResult"
+            &/ s3Elem' "AssumedRoleUser"
+            &/ s3Elem' "AssumedRoleId"
+            &/ content
+
+      convSB :: Text -> BA.ScrubbedBytes
+      convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
+
+      credsInfo = do
+        cr <-
+          maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
+            listToMaybe $
+              r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
+        let cur = fromNode $ node cr
+        return
+          ( CredentialValue
+              { cvAccessKey =
+                  coerce $
+                    T.concat $
+                      cur $/ s3Elem' "AccessKeyId" &/ content,
+                cvSecretKey =
+                  coerce $
+                    convSB $
+                      T.concat $
+                        cur
+                          $/ s3Elem' "SecretAccessKey"
+                          &/ content,
+                cvSessionToken =
+                  Just $
+                    coerce $
+                      convSB $
+                        T.concat $
+                          cur
+                            $/ s3Elem' "SessionToken"
+                            &/ content
+              },
+            T.concat $ cur $/ s3Elem' "Expiration" &/ content
+          )
+  creds <- either throwIO pure credsInfo
+  expiry <- parseS3XMLTime $ snd creds
+  let roleCredentials =
+        AssumeRoleCredentials
+          { arcCredentials = fst creds,
+            arcExpiration = expiry
+          }
+  return
+    AssumeRoleResult
+      { arrSourceIdentity = sourceIdentity,
+        arrAssumedRoleArn = roleArn,
+        arrAssumedRoleId = roleId,
+        arrRoleCredentials = roleCredentials
+      }
+
+instance STSCredentialProvider STSAssumeRole where
+  getSTSEndpoint = saroEndpoint . sarOptions
+  retrieveSTSCredentials sar (host', port', isSecure') mgr = do
+    -- Assemble STS request
+    let requiredParams =
+          [ ("Action", "AssumeRole"),
+            ("Version", stsVersion)
+          ]
+        opts = sarOptions sar
+
+        durSecs :: Int =
+          fromIntegral $
+            fromMaybe defaultDurationSeconds $
+              saroDurationSeconds opts
+        otherParams =
+          [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
+            ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
+            Just ("DurationSeconds", show durSecs),
+            ("Policy",) <$> saroPolicyJSON opts
+          ]
+        parameters = requiredParams ++ catMaybes otherParams
+        (host, port, isSecure) =
+          case getSTSEndpoint sar of
+            Just ep ->
+              let endPt = NC.parseRequest_ $ toString ep
+               in (NC.host endPt, NC.port endPt, NC.secure endPt)
+            Nothing -> (host', port', isSecure')
+        reqBody = renderSimpleQuery False parameters
+        req =
+          NC.defaultRequest
+            { NC.host = host,
+              NC.port = port,
+              NC.secure = isSecure,
+              NC.method = methodPost,
+              NC.requestHeaders =
+                [ (hHost, getHostHeader (host, port)),
+                  (hContentType, "application/x-www-form-urlencoded")
+                ],
+              NC.requestBody = RequestBodyBS reqBody
+            }
+
+    -- Sign the STS request.
+    timeStamp <- liftIO Time.getCurrentTime
+    let sp =
+          SignParams
+            { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
+              spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
+              spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
+              spService = ServiceSTS,
+              spTimeStamp = timeStamp,
+              spRegion = saroLocation opts,
+              spExpirySecs = Nothing,
+              spPayloadHash = Just $ hashSHA256 reqBody
+            }
+        signHeaders = signV4 sp req
+        signedReq =
+          req
+            { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
+            }
+
+    -- Make the STS request
+    resp <- httpLbs signedReq mgr
+    result <-
+      parseSTSAssumeRoleResult
+        (toStrict $ NC.responseBody resp)
+        "https://sts.amazonaws.com/doc/2011-06-15/"
+    return
+      ( arcCredentials $ arrRoleCredentials result,
+        coerce $ arcExpiration $ arrRoleCredentials result
+      )
diff --git a/src/Network/Minio/Credentials/Types.hs b/src/Network/Minio/Credentials/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Minio/Credentials/Types.hs
@@ -0,0 +1,90 @@
+--
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE StrictData #-}
+
+module Network.Minio.Credentials.Types where
+
+import qualified Data.ByteArray as BA
+import Lib.Prelude (UTCTime)
+import qualified Network.HTTP.Client as NC
+
+-- | Access Key type.
+newtype AccessKey = AccessKey {unAccessKey :: Text}
+  deriving stock (Show)
+  deriving newtype (Eq, IsString, Semigroup, Monoid)
+
+-- | Secret Key type - has a show instance that does not print the value.
+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
+  deriving stock (Show)
+  deriving newtype (Eq, IsString, Semigroup, Monoid)
+
+-- | Session Token type - has a show instance that does not print the value.
+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
+  deriving stock (Show)
+  deriving newtype (Eq, IsString, Semigroup, Monoid)
+
+-- | Object storage credential data type. It has support for the optional
+-- [SessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
+-- for using temporary credentials requested via STS.
+--
+-- The show instance for this type does not print the value of secrets for
+-- security.
+--
+-- @since 1.7.0
+data CredentialValue = CredentialValue
+  { cvAccessKey :: AccessKey,
+    cvSecretKey :: SecretKey,
+    cvSessionToken :: Maybe SessionToken
+  }
+  deriving stock (Eq, Show)
+
+scrubbedToText :: BA.ScrubbedBytes -> Text
+scrubbedToText =
+  let b2t :: ByteString -> Text
+      b2t = decodeUtf8
+      s2b :: BA.ScrubbedBytes -> ByteString
+      s2b = BA.convert
+   in b2t . s2b
+
+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
+-- credential to files or other programs.
+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
+credentialValueText cv =
+  ( coerce $ cvAccessKey cv,
+    (scrubbedToText . coerce) $ cvSecretKey cv,
+    scrubbedToText . coerce <$> cvSessionToken cv
+  )
+
+-- | Endpoint represented by host, port and TLS enabled flag.
+type Endpoint = (ByteString, Int, Bool)
+
+-- | Typeclass for STS credential providers.
+--
+-- @since 1.7.0
+class STSCredentialProvider p where
+  retrieveSTSCredentials ::
+    p ->
+    -- | STS Endpoint (host, port, isSecure)
+    Endpoint ->
+    NC.Manager ->
+    IO (CredentialValue, ExpiryTime)
+  getSTSEndpoint :: p -> Maybe Text
+
+-- | 'ExpiryTime' represents a time at which a credential expires.
+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
+  deriving stock (Show)
+  deriving newtype (Eq)
diff --git a/src/Network/Minio/Data.hs b/src/Network/Minio/Data.hs
--- a/src/Network/Minio/Data.hs
+++ b/src/Network/Minio/Data.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE StrictData #-}
 {-# LANGUAGE TypeFamilies #-}
 
 module Network.Minio.Data where
@@ -34,9 +35,9 @@
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as LB
-import Data.CaseInsensitive (mk)
 import qualified Data.HashMap.Strict as H
 import qualified Data.Ini as Ini
+import qualified Data.List as List
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
 import Data.Time (defaultTimeLocale, formatTime)
@@ -53,6 +54,7 @@
     hRange,
   )
 import qualified Network.HTTP.Types as HT
+import Network.Minio.Credentials
 import Network.Minio.Data.Crypto
   ( encodeToBase64,
     hashMD5ToBase64,
@@ -62,11 +64,12 @@
   ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
     MinioErr (..),
   )
+import Network.Minio.Utils
 import System.Directory (doesFileExist, getHomeDirectory)
 import qualified System.Environment as Env
 import System.FilePath.Posix (combine)
-import Text.XML (Name (Name))
 import qualified UnliftIO as U
+import qualified UnliftIO.MVar as UM
 
 -- | max obj size is 5TiB
 maxObjectSize :: Int64
@@ -97,13 +100,29 @@
       ("us-west-2", "s3.us-west-2.amazonaws.com"),
       ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
       ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
+      ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
       ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
       ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
+      ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
       ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
       ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
+      ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
       ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
       ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
+      ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
       ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
+      ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
+      ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
+      ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
+      ("af-south-1", "s3.af-south-1.amazonaws.com"),
+      ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
+      ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
+      ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
+      ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
+      ("me-south-1", "s3.me-south-1.amazonaws.com"),
+      ("me-central-1", "s3.me-central-1.amazonaws.com"),
+      ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
+      ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
       ("sa-east-1", "s3.sa-east-1.amazonaws.com")
     ]
 
@@ -115,50 +134,46 @@
 data ConnectInfo = ConnectInfo
   { connectHost :: Text,
     connectPort :: Int,
-    connectAccessKey :: Text,
-    connectSecretKey :: Text,
+    connectCreds :: Creds,
     connectIsSecure :: Bool,
     connectRegion :: Region,
     connectAutoDiscoverRegion :: Bool,
     connectDisableTLSCertValidation :: Bool
   }
-  deriving stock (Eq, Show)
 
+getEndpoint :: ConnectInfo -> Endpoint
+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
+
 instance IsString ConnectInfo where
   fromString str =
     let req = NC.parseRequest_ str
      in ConnectInfo
           { connectHost = TE.decodeUtf8 $ NC.host req,
             connectPort = NC.port req,
-            connectAccessKey = "",
-            connectSecretKey = "",
+            connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
             connectIsSecure = NC.secure req,
             connectRegion = "",
             connectAutoDiscoverRegion = True,
             connectDisableTLSCertValidation = False
           }
 
--- | Contains access key and secret key to access object storage.
-data Credentials = Credentials
-  { cAccessKey :: Text,
-    cSecretKey :: Text
-  }
-  deriving stock (Eq, Show)
-
--- | A Provider is an action that may return Credentials. Providers
--- may be chained together using 'findFirst'.
-type Provider = IO (Maybe Credentials)
+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
+-- Loaders may be chained together using 'findFirst'.
+--
+-- @since 1.7.0
+type CredentialLoader = IO (Maybe CredentialValue)
 
--- | Combines the given list of providers, by calling each one in
--- order until Credentials are found.
-findFirst :: [Provider] -> Provider
+-- | Combines the given list of loaders, by calling each one in
+-- order until a 'CredentialValue' is returned.
+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
 findFirst [] = return Nothing
 findFirst (f : fs) = do
   c <- f
   maybe (findFirst fs) (return . Just) c
 
--- | This Provider loads `Credentials` from @~\/.aws\/credentials@
-fromAWSConfigFile :: Provider
+-- | This action returns a 'CredentialValue' populated from
+-- @~\/.aws\/credentials@
+fromAWSConfigFile :: CredentialLoader
 fromAWSConfigFile = do
   credsE <- runExceptT $ do
     homeDir <- lift getHomeDirectory
@@ -174,29 +189,28 @@
       ExceptT $
         return $
           Ini.lookupValue "default" "aws_secret_access_key" ini
-    return $ Credentials akey skey
+    return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
   return $ either (const Nothing) Just credsE
 
--- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
--- @AWS_SECRET_ACCESS_KEY@ environment variables.
-fromAWSEnv :: Provider
+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
+fromAWSEnv :: CredentialLoader
 fromAWSEnv = runMaybeT $ do
   akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
   skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
-  return $ Credentials (T.pack akey) (T.pack skey)
+  return $ CredentialValue (fromString akey) (fromString skey) Nothing
 
--- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
--- @MINIO_SECRET_KEY@ environment variables.
-fromMinioEnv :: Provider
+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
+-- and @MINIO_SECRET_KEY@ environment variables.
+fromMinioEnv :: CredentialLoader
 fromMinioEnv = runMaybeT $ do
   akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
   skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
-  return $ Credentials (T.pack akey) (T.pack skey)
+  return $ CredentialValue (fromString akey) (fromString skey) Nothing
 
--- | setCredsFrom retrieves access credentials from the first
--- `Provider` form the given list that succeeds and sets it in the
--- `ConnectInfo`.
-setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
+-- | setCredsFrom retrieves access credentials from the first action in the
+-- given list that succeeds and sets it in the 'ConnectInfo'.
+setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
 setCredsFrom ps ci = do
   pMay <- findFirst ps
   maybe
@@ -204,14 +218,21 @@
     (return . (`setCreds` ci))
     pMay
 
--- | setCreds sets the given `Credentials` in the `ConnectInfo`.
-setCreds :: Credentials -> ConnectInfo -> ConnectInfo
-setCreds (Credentials accessKey secretKey) connInfo =
+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
+setCreds cv connInfo =
   connInfo
-    { connectAccessKey = accessKey,
-      connectSecretKey = secretKey
+    { connectCreds = CredsStatic cv
     }
 
+-- | 'setSTSCredential' configures `ConnectInfo` to retrieve temporary
+-- credentials via the STS API on demand. It is automatically refreshed on
+-- expiry.
+setSTSCredential :: (STSCredentialProvider p) => p -> ConnectInfo -> IO ConnectInfo
+setSTSCredential p ci = do
+  store <- initSTSCredential p
+  return ci {connectCreds = CredsSTS store}
+
 -- | Set the S3 region parameter in the `ConnectInfo`
 setRegion :: Region -> ConnectInfo -> ConnectInfo
 setRegion r connInfo =
@@ -233,15 +254,7 @@
 disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
 
 getHostAddr :: ConnectInfo -> ByteString
-getHostAddr ci =
-  if port == 80 || port == 443
-    then encodeUtf8 host
-    else
-      encodeUtf8 $
-        T.concat [host, ":", show port]
-  where
-    port = connectPort ci
-    host = connectHost ci
+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
 
 -- | Default Google Compute Storage ConnectInfo. Works only for
 -- "Simple Migration" use-case with interoperability mode enabled on
@@ -264,7 +277,7 @@
 -- ConnectInfo. Credentials are already filled in.
 minioPlayCI :: ConnectInfo
 minioPlayCI =
-  let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
+  let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
    in setCreds playCreds $
         setRegion
           "us-east-1"
@@ -291,7 +304,7 @@
 
 -- | Validates that the given ByteString is 32 bytes long and creates
 -- an encryption key.
-mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
 mkSSECKey keyBytes
   | B.length keyBytes /= 32 =
       throwM MErrVInvalidEncryptionKeyLength
@@ -308,7 +321,7 @@
   -- argument is the optional KMS context that must have a
   -- `A.ToJSON` instance - please refer to the AWS S3 documentation
   -- for detailed information.
-  SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
+  SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
   -- | Specifies server-side encryption with customer provided
   -- key. The argument is the encryption key to be used.
   SSEC :: SSECKey -> SSE
@@ -366,24 +379,6 @@
 defaultPutObjectOptions :: PutObjectOptions
 defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
 
--- | If the given header name has the @X-Amz-Meta-@ prefix, it is
--- stripped and a Just is returned.
-userMetadataHeaderNameMaybe :: Text -> Maybe Text
-userMetadataHeaderNameMaybe k =
-  let prefix = T.toCaseFold "X-Amz-Meta-"
-      n = T.length prefix
-   in if T.toCaseFold (T.take n k) == prefix
-        then Just (T.drop n k)
-        else Nothing
-
-addXAmzMetaPrefix :: Text -> Text
-addXAmzMetaPrefix s
-  | isJust (userMetadataHeaderNameMaybe s) = s
-  | otherwise = "X-Amz-Meta-" <> s
-
-mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
-mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
-
 pooToHeaders :: PutObjectOptions -> [HT.Header]
 pooToHeaders poo =
   userMetadata
@@ -423,6 +418,29 @@
 -- | A type alias to represent a part-number for multipart upload
 type PartNumber = Int16
 
+-- | Select part sizes - the logic is that the minimum part-size will
+-- be 64MiB.
+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
+selectPartSizes size =
+  uncurry (List.zip3 [1 ..]) $
+    List.unzip $
+      loop 0 size
+  where
+    ceil :: Double -> Int64
+    ceil = ceiling
+    partSize =
+      max
+        minPartSize
+        ( ceil $
+            fromIntegral size
+              / fromIntegral maxMultipartParts
+        )
+    m = partSize
+    loop st sz
+      | st > sz = []
+      | st + m >= sz = [(st, sz - st)]
+      | otherwise = (st, m) : loop (st + m) sz
+
 -- | A type alias to represent an upload-id for multipart upload
 type UploadId = Text
 
@@ -963,13 +981,14 @@
 
 -- | An EventMessage represents each kind of message received from the server.
 data EventMessage
-  = ProgressEventMessage {emProgress :: Progress}
-  | StatsEventMessage {emStats :: Stats}
+  = ProgressEventMessage Progress
+  | StatsEventMessage Stats
   | RequestLevelErrorMessage
-      { emErrorCode :: Text,
-        emErrorMessage :: Text
-      }
-  | RecordPayloadEventMessage {emPayloadBytes :: ByteString}
+      Text
+      -- ^ Error code
+      Text
+      -- ^ Error message
+  | RecordPayloadEventMessage ByteString
   deriving stock (Show, Eq)
 
 data MsgHeaderName
@@ -1146,9 +1165,22 @@
   conn <- liftIO $ connect ci
   runMinioResWith conn m
 
-s3Name :: Text -> Text -> Name
-s3Name ns s = Name s (Just ns) Nothing
-
 -- | Format as per RFC 1123.
 formatRFC1123 :: UTCTime -> T.Text
 formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
+
+lookupRegionCache :: Bucket -> Minio (Maybe Region)
+lookupRegionCache b = do
+  rMVar <- asks mcRegionMap
+  rMap <- UM.readMVar rMVar
+  return $ H.lookup b rMap
+
+addToRegionCache :: Bucket -> Region -> Minio ()
+addToRegionCache b region = do
+  rMVar <- asks mcRegionMap
+  UM.modifyMVar_ rMVar $ return . H.insert b region
+
+deleteFromRegionCache :: Bucket -> Minio ()
+deleteFromRegionCache b = do
+  rMVar <- asks mcRegionMap
+  UM.modifyMVar_ rMVar $ return . H.delete b
diff --git a/src/Network/Minio/Data/Crypto.hs b/src/Network/Minio/Data/Crypto.hs
--- a/src/Network/Minio/Data/Crypto.hs
+++ b/src/Network/Minio/Data/Crypto.hs
@@ -43,26 +43,26 @@
 hashSHA256 :: ByteString -> ByteString
 hashSHA256 = digestToBase16 . hashWith SHA256
 
-hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
 hashSHA256FromSource src = do
   digest <- C.connect src sinkSHA256Hash
   return $ digestToBase16 digest
   where
     -- To help with type inference
-    sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
+    sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
     sinkSHA256Hash = sinkHash
 
 -- Returns MD5 hash hex encoded.
 hashMD5 :: ByteString -> ByteString
 hashMD5 = digestToBase16 . hashWith MD5
 
-hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
 hashMD5FromSource src = do
   digest <- C.connect src sinkMD5Hash
   return $ digestToBase16 digest
   where
     -- To help with type inference
-    sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
+    sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
     sinkMD5Hash = sinkHash
 
 hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -71,15 +71,15 @@
 hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
 hmacSHA256RawBS message key = convert $ hmacSHA256 message key
 
-digestToBS :: ByteArrayAccess a => a -> ByteString
+digestToBS :: (ByteArrayAccess a) => a -> ByteString
 digestToBS = convert
 
-digestToBase16 :: ByteArrayAccess a => a -> ByteString
+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
 digestToBase16 = convertToBase Base16
 
 -- Returns MD5 hash base 64 encoded.
-hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
 hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
 
-encodeToBase64 :: ByteArrayAccess a => a -> ByteString
+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
 encodeToBase64 = convertToBase Base64
diff --git a/src/Network/Minio/Data/Time.hs b/src/Network/Minio/Data/Time.hs
--- a/src/Network/Minio/Data/Time.hs
+++ b/src/Network/Minio/Data/Time.hs
@@ -27,6 +27,7 @@
 
 import Data.ByteString.Char8 (pack)
 import qualified Data.Time as Time
+import Data.Time.Format.ISO8601 (iso8601Show)
 import Lib.Prelude
 
 -- | Time to expire for a presigned URL. It interpreted as a number of
@@ -49,4 +50,4 @@
 awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
 
 iso8601TimeFormat :: UTCTime -> [Char]
-iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
+iso8601TimeFormat = iso8601Show
diff --git a/src/Network/Minio/Errors.hs b/src/Network/Minio/Errors.hs
--- a/src/Network/Minio/Errors.hs
+++ b/src/Network/Minio/Errors.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@
   | MErrVInvalidEncryptionKeyLength
   | MErrVStreamingBodyUnexpectedEOF
   | MErrVUnexpectedPayload
+  | MErrVSTSEndpointNotFound
   deriving stock (Show, Eq)
 
 instance Exception MErrV
diff --git a/src/Network/Minio/PresignedOperations.hs b/src/Network/Minio/PresignedOperations.hs
--- a/src/Network/Minio/PresignedOperations.hs
+++ b/src/Network/Minio/PresignedOperations.hs
@@ -1,7 +1,5 @@
-{-# LANGUAGE CPP #-}
-
 --
--- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -15,6 +13,7 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
+{-# LANGUAGE CPP #-}
 
 module Network.Minio.PresignedOperations
   ( UrlExpiry,
@@ -47,6 +46,7 @@
 import qualified Network.HTTP.Client as NClient
 import qualified Network.HTTP.Types as HT
 import Network.Minio.API (buildRequest)
+import Network.Minio.Credentials
 import Network.Minio.Data
 import Network.Minio.Data.Time
 import Network.Minio.Errors
@@ -299,32 +299,36 @@
 presignedPostPolicy p = do
   ci <- asks mcConnInfo
   signTime <- liftIO Time.getCurrentTime
+  mgr <- asks mcConnManager
+  cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
 
-  let extraConditions =
+  let extraConditions signParams =
         [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
           PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
           PPCEquals
             "x-amz-credential"
             ( T.intercalate
                 "/"
-                [ connectAccessKey ci,
-                  decodeUtf8 $ mkScope signTime region
+                [ coerce $ cvAccessKey cv,
+                  decodeUtf8 $ credentialScope signParams
                 ]
             )
         ]
-      ppWithCreds =
+      ppWithCreds signParams =
         p
-          { conditions = conditions p ++ extraConditions
+          { conditions = conditions p ++ extraConditions signParams
           }
       sp =
         SignParams
-          (connectAccessKey ci)
-          (connectSecretKey ci)
+          (coerce $ cvAccessKey cv)
+          (coerce $ cvSecretKey cv)
+          (coerce $ cvSessionToken cv)
+          ServiceS3
           signTime
           (Just $ connectRegion ci)
           Nothing
           Nothing
-      signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
+      signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
       -- compute form-data
       mkPair (PPCStartsWith k v) = Just (k, v)
       mkPair (PPCEquals k v) = Just (k, v)
@@ -334,12 +338,11 @@
           H.fromList $
             mapMaybe
               mkPair
-              (conditions ppWithCreds)
+              (conditions $ ppWithCreds sp)
       formData = formFromPolicy `H.union` signData
       -- compute POST upload URL
       bucket = H.lookupDefault "" "bucket" formData
       scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
-      region = connectRegion ci
       url =
         toStrictBS $
           toLazyByteString $
diff --git a/src/Network/Minio/S3API.hs b/src/Network/Minio/S3API.hs
--- a/src/Network/Minio/S3API.hs
+++ b/src/Network/Minio/S3API.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -14,6 +14,14 @@
 -- limitations under the License.
 --
 
+-- |
+-- Module:      Network.Minio.S3API
+-- Copyright:   (c) 2017-2023 MinIO Dev Team
+-- License:     Apache 2.0
+-- Maintainer:  MinIO Dev Team <dev@min.io>
+--
+-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
+-- and use this only if needed.
 module Network.Minio.S3API
   ( Region,
     getLocation,
diff --git a/src/Network/Minio/SelectAPI.hs b/src/Network/Minio/SelectAPI.hs
--- a/src/Network/Minio/SelectAPI.hs
+++ b/src/Network/Minio/SelectAPI.hs
@@ -119,7 +119,7 @@
 chunkSize :: Int
 chunkSize = 32 * 1024
 
-parseBinary :: Bin.Binary a => ByteString -> IO a
+parseBinary :: (Bin.Binary a) => ByteString -> IO a
 parseBinary b = do
   case Bin.decodeOrFail $ LB.fromStrict b of
     Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
@@ -135,7 +135,7 @@
   _ -> throwIO ESEInvalidHeaderType
 
 parseHeaders ::
-  MonadUnliftIO m =>
+  (MonadUnliftIO m) =>
   Word32 ->
   C.ConduitM ByteString a m [MessageHeader]
 parseHeaders 0 = return []
@@ -163,7 +163,7 @@
 
 -- readNBytes returns N bytes read from the string and throws an
 -- exception if N bytes are not present on the stream.
-readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
 readNBytes n = do
   b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
   if B.length b /= n
@@ -171,7 +171,7 @@
     else return b
 
 crcCheck ::
-  MonadUnliftIO m =>
+  (MonadUnliftIO m) =>
   C.ConduitM ByteString ByteString m ()
 crcCheck = do
   b <- readNBytes 12
@@ -208,7 +208,7 @@
         then accumulateYield n' c'
         else return c'
 
-handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
 handleMessage = do
   b1 <- readNBytes 4
   msgLen :: Word32 <- liftIO $ parseBinary b1
@@ -254,7 +254,7 @@
       passThrough $ n - B.length b
 
 selectProtoConduit ::
-  MonadUnliftIO m =>
+  (MonadUnliftIO m) =>
   C.ConduitT ByteString EventMessage m ()
 selectProtoConduit = crcCheck .| handleMessage
 
@@ -281,7 +281,7 @@
   return $ NC.responseBody resp .| selectProtoConduit
 
 -- | A helper conduit that returns only the record payload bytes.
-getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
 getPayloadBytes = do
   evM <- C.await
   case evM of
diff --git a/src/Network/Minio/Sign/V4.hs b/src/Network/Minio/Sign/V4.hs
--- a/src/Network/Minio/Sign/V4.hs
+++ b/src/Network/Minio/Sign/V4.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -15,9 +15,19 @@
 --
 {-# LANGUAGE BangPatterns #-}
 
-module Network.Minio.Sign.V4 where
+module Network.Minio.Sign.V4
+  ( SignParams (..),
+    signV4QueryParams,
+    signV4,
+    signV4PostPolicy,
+    signV4Stream,
+    Service (..),
+    credentialScope,
+  )
+where
 
 import qualified Conduit as C
+import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Base64 as Base64
 import qualified Data.ByteString.Char8 as B8
@@ -26,11 +36,14 @@
 import qualified Data.CaseInsensitive as CI
 import qualified Data.HashMap.Strict as Map
 import qualified Data.HashSet as Set
+import Data.List (partition)
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Time as Time
 import Lib.Prelude
 import qualified Network.HTTP.Conduit as NC
-import Network.HTTP.Types (Header, parseQuery)
+import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
 import qualified Network.HTTP.Types as H
+import Network.HTTP.Types.Header (RequestHeaders)
 import Network.Minio.Data.ByteString
 import Network.Minio.Data.Crypto
 import Network.Minio.Data.Time
@@ -49,20 +62,18 @@
         H.hUserAgent
       ]
 
-data SignV4Data = SignV4Data
-  { sv4SignTime :: UTCTime,
-    sv4Scope :: ByteString,
-    sv4CanonicalRequest :: ByteString,
-    sv4HeadersToSign :: [(ByteString, ByteString)],
-    sv4Output :: [(ByteString, ByteString)],
-    sv4StringToSign :: ByteString,
-    sv4SigningKey :: ByteString
-  }
-  deriving stock (Show)
+data Service = ServiceS3 | ServiceSTS
+  deriving stock (Eq, Show)
 
+toByteString :: Service -> ByteString
+toByteString ServiceS3 = "s3"
+toByteString ServiceSTS = "sts"
+
 data SignParams = SignParams
   { spAccessKey :: Text,
-    spSecretKey :: Text,
+    spSecretKey :: BA.ScrubbedBytes,
+    spSessionToken :: Maybe BA.ScrubbedBytes,
+    spService :: Service,
     spTimeStamp :: UTCTime,
     spRegion :: Maybe Text,
     spExpirySecs :: Maybe UrlExpiry,
@@ -70,23 +81,6 @@
   }
   deriving stock (Show)
 
-debugPrintSignV4Data :: SignV4Data -> IO ()
-debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
-  B8.putStrLn "SignV4Data:"
-  B8.putStr "Timestamp: " >> print t
-  B8.putStr "Scope: " >> B8.putStrLn s
-  B8.putStrLn "Canonical Request:"
-  B8.putStrLn cr
-  B8.putStr "Headers to Sign: " >> print h2s
-  B8.putStr "Output: " >> print o
-  B8.putStr "StringToSign: " >> B8.putStrLn sts
-  B8.putStr "SigningKey: " >> printBytes sk
-  B8.putStrLn "END of SignV4Data ========="
-  where
-    printBytes b = do
-      mapM_ (\x -> B.putStr $ B.singleton x <> " ") $ B.unpack b
-      B8.putStrLn ""
-
 mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
 mkAuthHeader accessKey scope signedHeaderKeys sign =
   let authValue =
@@ -102,6 +96,12 @@
           ]
    in (H.hAuthorization, authValue)
 
+data IsStreaming = IsStreamingLength Int64 | NotStreaming
+  deriving stock (Eq, Show)
+
+amzSecurityToken :: ByteString
+amzSecurityToken = "X-Amz-Security-Token"
+
 -- | Given SignParams and request details, including request method,
 -- request path, headers, query params and payload hash, generates an
 -- updated set of headers, including the x-amz-date header and the
@@ -114,36 +114,23 @@
 -- is being created. The expiry is interpreted as an integer number of
 -- seconds. The output will be the list of query-parameters to add to
 -- the request.
-signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
-signV4 !sp !req =
-  let region = fromMaybe "" $ spRegion sp
-      ts = spTimeStamp sp
-      scope = mkScope ts region
-      accessKey = encodeUtf8 $ spAccessKey sp
-      secretKey = encodeUtf8 $ spSecretKey sp
+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
+signV4QueryParams !sp !req =
+  let scope = credentialScope sp
       expiry = spExpirySecs sp
-      sha256Hdr =
-        ( "x-amz-content-sha256",
-          fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
-        )
-      -- headers to be added to the request
-      datePair = ("X-Amz-Date", awsTimeFormatBS ts)
-      computedHeaders =
-        NC.requestHeaders req
-          ++ if isJust expiry
-            then []
-            else map (first mk) [datePair, sha256Hdr]
-      headersToSign = getHeadersToSign computedHeaders
+
+      headersToSign = getHeadersToSign $ NC.requestHeaders req
       signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
       -- query-parameters to be added before signing for presigned URLs
       -- (i.e. when `isJust expiry`)
       authQP =
         [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
-          ("X-Amz-Credential", B.concat [accessKey, "/", scope]),
-          datePair,
+          ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
+          ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
           ("X-Amz-Expires", maybe "" showBS expiry),
           ("X-Amz-SignedHeaders", signedHeaderKeys)
         ]
+          ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
       finalQP =
         parseQuery (NC.queryString req)
           ++ if isJust expiry
@@ -156,40 +143,130 @@
           sp
           (NC.setQueryString finalQP req)
           headersToSign
+
       -- 2. compute string to sign
-      stringToSign = mkStringToSign ts scope canonicalRequest
+      stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
       -- 3.1 compute signing key
-      signingKey = mkSigningKey ts region secretKey
+      signingKey = getSigningKey sp
       -- 3.2 compute signature
       signature = computeSignature stringToSign signingKey
+   in ("X-Amz-Signature", signature) : authQP
+
+-- | Given SignParams and request details, including request method, request
+-- path, headers, query params and payload hash, generates an updated set of
+-- headers, including the x-amz-date header and the Authorization header, which
+-- includes the signature.
+--
+-- The output is the list of headers to be added to authenticate the request.
+signV4 :: SignParams -> NC.Request -> [Header]
+signV4 !sp !req =
+  let scope = credentialScope sp
+
+      -- extra headers to be added for signing purposes.
+      extraHeaders =
+        ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
+          : ( -- payload hash is only used for S3 (not STS)
+              [ ( "x-amz-content-sha256",
+                  fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
+                )
+                | spService sp == ServiceS3
+              ]
+            )
+          ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
+
+      -- 1. compute canonical request
+      reqHeaders = NC.requestHeaders req ++ extraHeaders
+      (canonicalRequest, signedHeaderKeys) =
+        getCanonicalRequestAndSignedHeaders
+          NotStreaming
+          sp
+          req
+          reqHeaders
+
+      -- 2. compute string to sign
+      stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
+      -- 3.1 compute signing key
+      signingKey = getSigningKey sp
+      -- 3.2 compute signature
+      signature = computeSignature stringToSign signingKey
       -- 4. compute auth header
       authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
-      -- finally compute output pairs
-      output =
-        if isJust expiry
-          then ("X-Amz-Signature", signature) : authQP
-          else
-            [ first CI.foldedCase authHeader,
-              datePair,
-              sha256Hdr
-            ]
-   in output
+   in authHeader : extraHeaders
 
-mkScope :: UTCTime -> Text -> ByteString
-mkScope ts region =
-  B.intercalate
-    "/"
-    [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
-      encodeUtf8 region,
-      "s3",
-      "aws4_request"
-    ]
+credentialScope :: SignParams -> ByteString
+credentialScope sp =
+  let region = fromMaybe "" $ spRegion sp
+   in B.intercalate
+        "/"
+        [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
+          encodeUtf8 region,
+          toByteString $ spService sp,
+          "aws4_request"
+        ]
 
+-- Folds header name, trims whitespace in header values, skips ignored headers
+-- and sorts headers.
 getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
 getHeadersToSign !h =
   filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
     map (bimap CI.foldedCase stripBS) h
 
+-- | Given the list of headers in the request, computes the canonical headers
+-- and the signed headers strings.
+getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
+getCanonicalHeaders h =
+  let -- Folds header name, trims spaces in header values, skips ignored
+      -- headers and sorts headers by name (we must not re-order multi-valued
+      -- headers).
+      headersToSign =
+        NE.toList $
+          NE.sortBy (\a b -> compare (fst a) (fst b)) $
+            NE.fromList $
+              NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
+                NE.map (bimap CI.foldedCase stripBS) h
+
+      canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
+      signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
+   in (canonicalHeaders, signedHeaderKeys)
+
+getCanonicalRequestAndSignedHeaders ::
+  IsStreaming ->
+  SignParams ->
+  NC.Request ->
+  [Header] ->
+  (ByteString, ByteString)
+getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
+  let httpMethod = NC.method req
+
+      canonicalUri = uriEncode False $ NC.path req
+
+      canonicalQueryString =
+        B.intercalate "&" $
+          map (\(x, y) -> B.concat [x, "=", y]) $
+            sort $
+              map
+                ( bimap (uriEncode True) (maybe "" (uriEncode True))
+                )
+                (parseQuery $ NC.queryString req)
+
+      (canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
+      payloadHashStr =
+        case isStreaming of
+          IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
+          NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
+
+      canonicalRequest =
+        B.intercalate
+          "\n"
+          [ httpMethod,
+            canonicalUri,
+            canonicalQueryString,
+            canonicalHeaders,
+            signedHeaderKeys,
+            payloadHashStr
+          ]
+   in (canonicalRequest, signedHeaderKeys)
+
 mkCanonicalRequest ::
   Bool ->
   SignParams ->
@@ -197,10 +274,12 @@
   [(ByteString, ByteString)] ->
   ByteString
 mkCanonicalRequest !isStreaming !sp !req !headersForSign =
-  let canonicalQueryString =
+  let httpMethod = NC.method req
+      canonicalUri = uriEncode False $ NC.path req
+      canonicalQueryString =
         B.intercalate "&" $
           map (\(x, y) -> B.concat [x, "=", y]) $
-            sort $
+            sortBy (\a b -> compare (fst a) (fst b)) $
               map
                 ( bimap (uriEncode True) (maybe "" (uriEncode True))
                 )
@@ -216,8 +295,8 @@
           else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
    in B.intercalate
         "\n"
-        [ NC.method req,
-          uriEncode False $ NC.path req,
+        [ httpMethod,
+          canonicalUri,
           canonicalQueryString,
           canonicalHeaders,
           signedHeaders,
@@ -234,13 +313,13 @@
       hashSHA256 canonicalRequest
     ]
 
-mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
-mkSigningKey ts region !secretKey =
+getSigningKey :: SignParams -> ByteString
+getSigningKey sp =
   hmacSHA256RawBS "aws4_request"
-    . hmacSHA256RawBS "s3"
-    . hmacSHA256RawBS (encodeUtf8 region)
-    . hmacSHA256RawBS (awsDateFormatBS ts)
-    $ B.concat ["AWS4", secretKey]
+    . hmacSHA256RawBS (toByteString $ spService sp)
+    . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
+    . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
+    $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
 
 computeSignature :: ByteString -> ByteString -> ByteString
 computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -254,20 +333,20 @@
   Map.HashMap Text ByteString
 signV4PostPolicy !postPolicyJSON !sp =
   let stringToSign = Base64.encode postPolicyJSON
-      region = fromMaybe "" $ spRegion sp
-      signingKey = mkSigningKey (spTimeStamp sp) region $ encodeUtf8 $ spSecretKey sp
+      signingKey = getSigningKey sp
       signature = computeSignature stringToSign signingKey
-   in Map.fromList
+   in Map.fromList $
         [ ("x-amz-signature", signature),
           ("policy", stringToSign)
         ]
+          ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
 
 chunkSizeConstant :: Int
 chunkSizeConstant = 64 * 1024
 
 -- base16Len computes the number of bytes required to represent @n (> 0)@ in
 -- hexadecimal.
-base16Len :: Integral a => a -> Int
+base16Len :: (Integral a) => a -> Int
 base16Len n
   | n == 0 = 0
   | otherwise = 1 + base16Len (n `div` 16)
@@ -284,60 +363,60 @@
       finalChunkSize = 1 + 17 + 64 + 2 + 2
    in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
 
+-- For streaming S3, we need to update the content-encoding header.
+addContentEncoding :: [Header] -> [Header]
+addContentEncoding hs =
+  -- assume there is at most one content-encoding header.
+  let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
+   in maybe
+        (hContentEncoding, "aws-chunked")
+        (\(k, v) -> (k, v <> ",aws-chunked"))
+        (listToMaybe ceHdrs)
+        : others
+
 signV4Stream ::
   Int64 ->
   SignParams ->
   NC.Request ->
   (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
--- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
 signV4Stream !payloadLength !sp !req =
   let ts = spTimeStamp sp
-      addContentEncoding hs =
-        let ceMay = find (\(x, _) -> x == "content-encoding") hs
-         in case ceMay of
-              Nothing -> ("content-encoding", "aws-chunked") : hs
-              Just (_, ce) ->
-                ("content-encoding", ce <> ",aws-chunked")
-                  : filter (\(x, _) -> x /= "content-encoding") hs
-      -- headers to be added to the request
-      datePair = ("X-Amz-Date", awsTimeFormatBS ts)
-      computedHeaders =
-        addContentEncoding $
-          datePair : NC.requestHeaders req
-      -- headers specific to streaming signature
+
+      -- compute the updated list of headers to be added for signing purposes.
       signedContentLength = signedStreamLength payloadLength
-      streamingHeaders :: [Header]
-      streamingHeaders =
-        [ ("x-amz-decoded-content-length", showBS payloadLength),
+      extraHeaders =
+        [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
+          ("x-amz-decoded-content-length", showBS payloadLength),
           ("content-length", showBS signedContentLength),
           ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
         ]
-      headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
-      signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
-      finalQP = parseQuery (NC.queryString req)
+          ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
+      requestHeaders =
+        addContentEncoding $
+          foldr setHeader (NC.requestHeaders req) extraHeaders
+
       -- 1. Compute Seed Signature
       -- 1.1 Canonical Request
-      canonicalReq =
-        mkCanonicalRequest
-          True
+      (canonicalReq, signedHeaderKeys) =
+        getCanonicalRequestAndSignedHeaders
+          (IsStreamingLength payloadLength)
           sp
-          (NC.setQueryString finalQP req)
-          headersToSign
-      region = fromMaybe "" $ spRegion sp
-      scope = mkScope ts region
+          req
+          requestHeaders
+
+      scope = credentialScope sp
       accessKey = spAccessKey sp
-      secretKey = spSecretKey sp
       -- 1.2 String toSign
       stringToSign = mkStringToSign ts scope canonicalReq
       -- 1.3 Compute signature
       -- 1.3.1 compute signing key
-      signingKey = mkSigningKey ts region $ encodeUtf8 secretKey
+      signingKey = getSigningKey sp
       -- 1.3.2 Compute signature
       seedSignature = computeSignature stringToSign signingKey
       -- 1.3.3 Compute Auth Header
       authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
       -- 1.4 Updated headers for the request
-      finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
+      finalReqHeaders = authHeader : requestHeaders
       -- headersToAdd = authHeader : datePair : streamingHeaders
 
       toHexStr n = B8.pack $ printf "%x" n
@@ -407,3 +486,9 @@
               NC.requestBodySource signedContentLength $
                 src C..| signerConduit numParts lastPSize seedSignature
           }
+
+-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
+setHeader :: Header -> RequestHeaders -> RequestHeaders
+setHeader hdr r =
+  let r' = filter (\(name, _) -> name /= fst hdr) r
+   in hdr : r'
diff --git a/src/Network/Minio/Utils.hs b/src/Network/Minio/Utils.hs
--- a/src/Network/Minio/Utils.hs
+++ b/src/Network/Minio/Utils.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@
 import Data.CaseInsensitive (mk, original)
 import qualified Data.Conduit.Binary as CB
 import qualified Data.HashMap.Strict as H
-import qualified Data.List as List
 import qualified Data.Text as T
 import Data.Text.Read (decimal)
 import Data.Time
@@ -37,14 +36,12 @@
 import qualified Network.HTTP.Conduit as NC
 import qualified Network.HTTP.Types as HT
 import qualified Network.HTTP.Types.Header as Hdr
-import Network.Minio.Data
 import Network.Minio.Data.ByteString
 import Network.Minio.JsonParser (parseErrResponseJSON)
-import Network.Minio.XmlParser (parseErrResponse)
+import Network.Minio.XmlCommon (parseErrResponse)
 import qualified System.IO as IO
 import qualified UnliftIO as U
 import qualified UnliftIO.Async as A
-import qualified UnliftIO.MVar as UM
 
 allocateReadFile ::
   (MonadUnliftIO m, R.MonadResource m) =>
@@ -115,6 +112,16 @@
 getMetadata =
   map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
 
+-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
+-- stripped and a Just is returned.
+userMetadataHeaderNameMaybe :: Text -> Maybe Text
+userMetadataHeaderNameMaybe k =
+  let prefix = T.toCaseFold "X-Amz-Meta-"
+      n = T.length prefix
+   in if T.toCaseFold (T.take n k) == prefix
+        then Just (T.drop n k)
+        else Nothing
+
 toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
 toMaybeMetadataHeader (k, v) =
   (,v) <$> userMetadataHeaderNameMaybe k
@@ -128,6 +135,14 @@
           . fst
       )
 
+addXAmzMetaPrefix :: Text -> Text
+addXAmzMetaPrefix s
+  | isJust (userMetadataHeaderNameMaybe s) = s
+  | otherwise = "X-Amz-Meta-" <> s
+
+mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
+
 -- | This function collects all headers starting with `x-amz-meta-`
 -- and strips off this prefix, and returns a map.
 getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
@@ -135,6 +150,12 @@
   H.fromList
     . mapMaybe toMaybeMetadataHeader
 
+getHostHeader :: (ByteString, Int) -> ByteString
+getHostHeader (host_, port_) =
+  if port_ == 80 || port_ == 443
+    then host_
+    else host_ <> ":" <> show port_
+
 getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
 getLastModifiedHeader hs = do
   modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -154,7 +175,7 @@
    in (s >= 200 && s < 300)
 
 httpLbs ::
-  MonadIO m =>
+  (MonadIO m) =>
   NC.Request ->
   NC.Manager ->
   m (NC.Response LByteString)
@@ -218,7 +239,7 @@
 -- Similar to mapConcurrently but limits the number of threads that
 -- can run using a quantity semaphore.
 limitedMapConcurrently ::
-  MonadUnliftIO m =>
+  (MonadUnliftIO m) =>
   Int ->
   (t -> m a) ->
   [t] ->
@@ -262,42 +283,3 @@
       | B.length bs == s -> C.yield bs >> chunkBSConduit ss
       | B.length bs > 0 -> C.yield bs
       | otherwise -> return ()
-
--- | Select part sizes - the logic is that the minimum part-size will
--- be 64MiB.
-selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
-selectPartSizes size =
-  uncurry (List.zip3 [1 ..]) $
-    List.unzip $
-      loop 0 size
-  where
-    ceil :: Double -> Int64
-    ceil = ceiling
-    partSize =
-      max
-        minPartSize
-        ( ceil $
-            fromIntegral size
-              / fromIntegral maxMultipartParts
-        )
-    m = partSize
-    loop st sz
-      | st > sz = []
-      | st + m >= sz = [(st, sz - st)]
-      | otherwise = (st, m) : loop (st + m) sz
-
-lookupRegionCache :: Bucket -> Minio (Maybe Region)
-lookupRegionCache b = do
-  rMVar <- asks mcRegionMap
-  rMap <- UM.readMVar rMVar
-  return $ H.lookup b rMap
-
-addToRegionCache :: Bucket -> Region -> Minio ()
-addToRegionCache b region = do
-  rMVar <- asks mcRegionMap
-  UM.modifyMVar_ rMVar $ return . H.insert b region
-
-deleteFromRegionCache :: Bucket -> Minio ()
-deleteFromRegionCache b = do
-  rMVar <- asks mcRegionMap
-  UM.modifyMVar_ rMVar $ return . H.delete b
diff --git a/src/Network/Minio/XmlCommon.hs b/src/Network/Minio/XmlCommon.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Minio/XmlCommon.hs
@@ -0,0 +1,65 @@
+--
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+module Network.Minio.XmlCommon where
+
+import qualified Data.Text as T
+import Data.Text.Read (decimal)
+import Data.Time (UTCTime)
+import Data.Time.Format.ISO8601 (iso8601ParseM)
+import Lib.Prelude (throwIO)
+import Network.Minio.Errors
+import Text.XML (Name (Name), def, parseLBS)
+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
+
+s3Name :: Text -> Text -> Name
+s3Name ns s = Name s (Just ns) Nothing
+
+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
+uncurry4 f (a, b, c, d) = f a b c d
+
+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
+
+-- | Parse time strings from XML
+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
+parseS3XMLTime t =
+  maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
+    iso8601ParseM $
+      toString t
+
+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
+parseDecimal numStr =
+  either (throwIO . MErrVXmlParse . show) return $
+    fst <$> decimal numStr
+
+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
+parseDecimals numStr = forM numStr parseDecimal
+
+s3Elem :: Text -> Text -> Axis
+s3Elem ns = element . s3Name ns
+
+parseRoot :: (MonadIO m) => LByteString -> m Cursor
+parseRoot =
+  either (throwIO . MErrVXmlParse . show) (return . fromDocument)
+    . parseLBS def
+
+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
+parseErrResponse xmldata = do
+  r <- parseRoot xmldata
+  let code = T.concat $ r $/ laxElement "Code" &/ content
+      message = T.concat $ r $/ laxElement "Message" &/ content
+  return $ toServiceErr code message
diff --git a/src/Network/Minio/XmlGenerator.hs b/src/Network/Minio/XmlGenerator.hs
--- a/src/Network/Minio/XmlGenerator.hs
+++ b/src/Network/Minio/XmlGenerator.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
 import qualified Data.ByteString.Lazy as LBS
 import qualified Data.Text as T
 import Network.Minio.Data
+import Network.Minio.XmlCommon
 import Text.XML
 
 -- | Create a bucketConfig request body XML
diff --git a/src/Network/Minio/XmlParser.hs b/src/Network/Minio/XmlParser.hs
--- a/src/Network/Minio/XmlParser.hs
+++ b/src/Network/Minio/XmlParser.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -34,48 +34,11 @@
 import qualified Data.HashMap.Strict as H
 import Data.List (zip4, zip6)
 import qualified Data.Text as T
-import Data.Text.Read (decimal)
 import Data.Time
-import Lib.Prelude
 import Network.Minio.Data
-import Network.Minio.Errors
-import Text.XML
+import Network.Minio.XmlCommon
 import Text.XML.Cursor hiding (bool)
 
--- | Represent the time format string returned by S3 API calls.
-s3TimeFormat :: [Char]
-s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
-
--- | Helper functions.
-uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
-uncurry4 f (a, b, c, d) = f a b c d
-
-uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
-uncurry6 f (a, b, c, d, e, g) = f a b c d e g
-
--- | Parse time strings from XML
-parseS3XMLTime :: MonadIO m => Text -> m UTCTime
-parseS3XMLTime t =
-  maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
-    parseTimeM True defaultTimeLocale s3TimeFormat $
-      T.unpack t
-
-parseDecimal :: (MonadIO m, Integral a) => Text -> m a
-parseDecimal numStr =
-  either (throwIO . MErrVXmlParse . show) return $
-    fst <$> decimal numStr
-
-parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
-parseDecimals numStr = forM numStr parseDecimal
-
-s3Elem :: Text -> Text -> Axis
-s3Elem ns = element . s3Name ns
-
-parseRoot :: (MonadIO m) => LByteString -> m Cursor
-parseRoot =
-  either (throwIO . MErrVXmlParse . show) (return . fromDocument)
-    . parseLBS def
-
 -- | Parse the response XML of a list buckets call.
 parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
 parseListBuckets xmldata = do
@@ -220,13 +183,6 @@
 
   return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
 
-parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
-parseErrResponse xmldata = do
-  r <- parseRoot xmldata
-  let code = T.concat $ r $/ element "Code" &/ content
-      message = T.concat $ r $/ element "Message" &/ content
-  return $ toServiceErr code message
-
 parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
 parseNotification xmldata = do
   r <- parseRoot xmldata
@@ -262,7 +218,7 @@
           events
           (Filter $ FilterKey $ FilterRules rules)
 
-parseSelectProgress :: MonadIO m => ByteString -> m Progress
+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
 parseSelectProgress xmldata = do
   r <- parseRoot $ LB.fromStrict xmldata
   let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
diff --git a/test/LiveServer.hs b/test/LiveServer.hs
--- a/test/LiveServer.hs
+++ b/test/LiveServer.hs
@@ -1,7 +1,5 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 --
--- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -32,6 +30,7 @@
 import qualified Network.HTTP.Conduit as NC
 import qualified Network.HTTP.Types as HT
 import Network.Minio
+import Network.Minio.Credentials (Creds (CredsStatic))
 import Network.Minio.Data
 import Network.Minio.Data.Crypto
 import Network.Minio.S3API
@@ -51,7 +50,7 @@
 tests = testGroup "Tests" [liveServerUnitTests]
 
 -- conduit that generates random binary stream of given length
-randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
 randomDataSrc = genBS
   where
     concatIt bs n =
@@ -69,7 +68,7 @@
           yield $ concatIt byteArr64 oneMiB
           genBS (s - oneMiB)
 
-mkRandFile :: R.MonadResource m => Int64 -> m FilePath
+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
 mkRandFile size = do
   dir <- liftIO getTemporaryDirectory
   C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
@@ -77,15 +76,35 @@
 funTestBucketPrefix :: Text
 funTestBucketPrefix = "miniohstest-"
 
-loadTestServer :: IO ConnectInfo
-loadTestServer = do
+loadTestServerConnInfo :: IO ConnectInfo
+loadTestServerConnInfo = do
   val <- Env.lookupEnv "MINIO_LOCAL"
   isSecure <- Env.lookupEnv "MINIO_SECURE"
   return $ case (val, isSecure) of
-    (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
-    (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
+    (Just _, Just _) -> setCreds (CredentialValue "minio" "minio123" mempty) "https://localhost:9000"
+    (Just _, Nothing) -> setCreds (CredentialValue "minio" "minio123" mempty) "http://localhost:9000"
     (Nothing, _) -> minioPlayCI
 
+loadTestServerConnInfoSTS :: IO ConnectInfo
+loadTestServerConnInfoSTS = do
+  val <- Env.lookupEnv "MINIO_LOCAL"
+  isSecure <- Env.lookupEnv "MINIO_SECURE"
+  let cv = CredentialValue "minio" "minio123" mempty
+      assumeRole =
+        STSAssumeRole
+          { sarCredentials = cv,
+            sarOptions = defaultSTSAssumeRoleOptions
+          }
+  case (val, isSecure) of
+    (Just _, Just _) -> setSTSCredential assumeRole "https://localhost:9000"
+    (Just _, Nothing) -> setSTSCredential assumeRole "http://localhost:9000"
+    (Nothing, _) -> do
+      cv' <- case connectCreds minioPlayCI of
+        CredsStatic c -> return c
+        _ -> error "unexpected play creds"
+      let assumeRole' = assumeRole {sarCredentials = cv'}
+      setSTSCredential assumeRole' minioPlayCI
+
 funTestWithBucket ::
   TestName ->
   (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
@@ -95,7 +114,7 @@
   bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
   let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
       liftStep = liftIO . step
-  connInfo <- loadTestServer
+  connInfo <- loadTestServerConnInfo
   ret <- runMinio connInfo $ do
     liftStep $ "Creating bucket for test - " ++ t
     foundBucket <- bucketExists b
@@ -105,6 +124,17 @@
     deleteBucket b
   isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
 
+  connInfoSTS <- loadTestServerConnInfoSTS
+  let t' = t ++ " (with AssumeRole Credentials)"
+  ret' <- runMinio connInfoSTS $ do
+    liftStep $ "Creating bucket for test - " ++ t'
+    foundBucket <- bucketExists b
+    liftIO $ foundBucket @?= False
+    makeBucket b Nothing
+    minioTest liftStep b
+    deleteBucket b
+  isRight ret' @? ("Functional test " ++ t' ++ " failed => " ++ show ret')
+
 liveServerUnitTests :: TestTree
 liveServerUnitTests =
   testGroup
@@ -125,7 +155,8 @@
       presignedUrlFunTest,
       presignedPostPolicyFunTest,
       bucketPolicyFunTest,
-      getNPutSSECTest
+      getNPutSSECTest,
+      assumeRoleRequestTest
     ]
 
 basicTests :: TestTree
@@ -279,7 +310,8 @@
     fGetObject bucket object destFile defaultGetObjectOptions
     gotSize <- withNewHandle destFile getFileSize
     liftIO $
-      gotSize == Right (Just mb15)
+      gotSize
+        == Right (Just mb15)
         @? "Wrong file size of put file after getting"
 
     step "Cleanup actions"
@@ -303,7 +335,8 @@
     fGetObject bucket obj destFile defaultGetObjectOptions
     gotSize <- withNewHandle destFile getFileSize
     liftIO $
-      gotSize == Right (Just mb1)
+      gotSize
+        == Right (Just mb1)
         @? "Wrong file size of put file after getting"
 
     step "Cleanup actions"
@@ -327,7 +360,8 @@
     fGetObject bucket obj destFile defaultGetObjectOptions
     gotSize <- withNewHandle destFile getFileSize
     liftIO $
-      gotSize == Right (Just mb70)
+      gotSize
+        == Right (Just mb70)
         @? "Wrong file size of put file after getting"
 
     step "Cleanup actions"
@@ -569,6 +603,7 @@
         []
         []
 
+    print putUrl
     let size1 = 1000 :: Int64
     inputFile <- mkRandFile size1
 
@@ -1176,9 +1211,37 @@
 
         gotSize <- withNewHandle dstFile getFileSize
         liftIO $
-          gotSize == Right (Just mb1)
+          gotSize
+            == Right (Just mb1)
             @? "Wrong file size of object when getting"
 
         step "Cleanup"
         deleteObject bucket obj
       else step "Skipping encryption test as server is not using TLS"
+
+assumeRoleRequestTest :: TestTree
+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
+  step "Load credentials"
+  val <- Env.lookupEnv "MINIO_LOCAL"
+  isSecure <- Env.lookupEnv "MINIO_SECURE"
+  let localMinioCred = Just $ CredentialValue "minio" "minio123" mempty
+      playCreds =
+        case connectCreds minioPlayCI of
+          CredsStatic c -> Just c
+          _ -> Nothing
+      (cvMay, loc) =
+        case (val, isSecure) of
+          (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
+          (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
+          (Nothing, _) -> (playCreds, "https://play.min.io:9000")
+  cv <- maybe (assertFailure "bad creds") return cvMay
+  let assumeRole =
+        STSAssumeRole cv $
+          defaultSTSAssumeRoleOptions
+            { saroLocation = Just "us-east-1",
+              saroEndpoint = Just loc
+            }
+  step "AssumeRole request"
+  res <- requestSTSCredential assumeRole
+  let v = credentialValueText $ fst res
+  print (v, snd res)
diff --git a/test/Network/Minio/XmlGenerator/Test.hs b/test/Network/Minio/XmlGenerator/Test.hs
--- a/test/Network/Minio/XmlGenerator/Test.hs
+++ b/test/Network/Minio/XmlGenerator/Test.hs
@@ -20,6 +20,7 @@
   )
 where
 
+import qualified Data.ByteString.Lazy as LBS
 import Lib.Prelude
 import Network.Minio.Data
 import Network.Minio.TestHelpers
@@ -28,6 +29,7 @@
 import Test.Tasty
 import Test.Tasty.HUnit
 import Text.RawString.QQ (r)
+import Text.XML (def, parseLBS)
 
 xmlGeneratorTests :: TestTree
 xmlGeneratorTests =
@@ -120,7 +122,13 @@
 testMkSelectRequest :: Assertion
 testMkSelectRequest = mapM_ assertFn cases
   where
-    assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
+    assertFn (a, b) =
+      let generatedReqDoc = parseLBS def $ LBS.fromStrict $ mkSelectRequest a
+          expectedReqDoc = parseLBS def $ LBS.fromStrict b
+       in case (generatedReqDoc, expectedReqDoc) of
+            (Right genDoc, Right expDoc) -> assertEqual "selectRequest XML should match: " expDoc genDoc
+            (Left err, _) -> assertFailure $ "Generated selectRequest failed to parse as XML" ++ show err
+            (_, Left err) -> assertFailure $ "Expected selectRequest failed to parse as XML" ++ show err
     cases =
       [ ( SelectRequest
             "Select * from S3Object"
@@ -143,8 +151,8 @@
                   <> quoteEscapeCharacter "\""
             )
             (Just False),
-          [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><RecordDelimiter>
-</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
+          [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
 </RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
         ),
         ( setRequestProgressEnabled False $
@@ -168,7 +176,7 @@
                       <> quoteCharacter "\""
                       <> quoteEscapeCharacter "\""
                 ),
-          [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
+          [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
 </RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
         )
       ]
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,5 +1,5 @@
 --
--- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
 -- you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@
 import Network.Minio.API.Test
 import Network.Minio.CopyObject
 import Network.Minio.Data
-import Network.Minio.PutObject
 import Network.Minio.Utils.Test
 import Network.Minio.XmlGenerator.Test
 import Network.Minio.XmlParser.Test
