diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index f028486b018efbb11e43c23e482cbd6fd8b86935..b33a1249d2f7a3027eeff13ad721b0c1db1850b3 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -266,6 +266,7 @@
     metabase.api                   :any
     metabase.async                 :any
     metabase.audit                 :any
+    metabase.auth-provider         #{metabase.util}
     metabase.bootstrap             #{}
     metabase.cmd                   :any
     metabase.compatibility         :any
diff --git a/docs/developers-guide/driver-changelog.md b/docs/developers-guide/driver-changelog.md
index 3ed350c4be774bfd4426baceda9dd859a3b0ca23..b693024126c295faaae7dd2181d0fc8a445ae743 100644
--- a/docs/developers-guide/driver-changelog.md
+++ b/docs/developers-guide/driver-changelog.md
@@ -113,6 +113,12 @@ title: Driver interface changelog
 -  The`:parameterized-sql` driver feature has been added to distinguish drivers that don't support parametrized SQL in
    tests. Currently, this is disabled only for `:sparksql`.
 
+## Metabase 0.50.17
+
+- Added method `metabase.driver/incorporate-auth-provider-details` for driver specific behavior required to
+  incorporate response of an auth-provider into the DB details.  In most cases this means setting the :password
+  and/or :username based on the auth-provider and its response.
+
 ## Metabase 0.50.16
 
 - `:type/fingerprinting-unsupported` has been added in the `metabase.types` namespace. Similar to
diff --git a/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.styled.tsx b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d96f2d5ef178a714b3e7834b77e4a461669d968e
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.styled.tsx
@@ -0,0 +1,14 @@
+import styled from "@emotion/styled";
+
+import Button from "metabase/core/components/Button";
+
+export const SectionButton = styled(Button)`
+  color: var(--mb-color-brand);
+  padding: 0;
+  border: none;
+  border-radius: 0;
+
+  &:hover {
+    background-color: transparent;
+  }
+`;
diff --git a/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.tsx b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b19f046acddda06085f61b0270094fd5ac36463e
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/DatabaseAuthProviderSectionField.tsx
@@ -0,0 +1,32 @@
+import { useField } from "formik";
+import { useCallback } from "react";
+import { t } from "ttag";
+
+import FormField from "metabase/core/components/FormField";
+
+import { SectionButton } from "./DatabaseAuthProviderSectionField.styled";
+
+export interface DatabaseAuthProviderSectionFieldProps {
+  name: string;
+}
+
+const DatabaseAuthProviderSectionField = ({
+  name,
+}: DatabaseAuthProviderSectionFieldProps): JSX.Element => {
+  const [{ value }, , { setValue }] = useField(name);
+
+  const handleClick = useCallback(() => {
+    setValue(!value);
+  }, [value, setValue]);
+
+  return (
+    <FormField>
+      <SectionButton type="button" onClick={handleClick}>
+        {value ? t`Use password` : t`Use an authentication provider`}
+      </SectionButton>
+    </FormField>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export -- deprecated usage
+export default DatabaseAuthProviderSectionField;
diff --git a/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/index.ts b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..067776c5306a3406b027760cbf57396cc3fe09b6
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseAuthProviderSectionField/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/no-default-export -- deprecated usage
+export { default } from "./DatabaseAuthProviderSectionField";
diff --git a/frontend/src/metabase/databases/components/DatabaseHostnameSectionField/DatabaseHostameSectionField.tsx b/frontend/src/metabase/databases/components/DatabaseHostnameSectionField/DatabaseHostameSectionField.tsx
index 9b5a1b131541d622843a448ee7b5abaf83ee6af1..dddafc631cbf3a6ab62e3ed7cd4dd17852bbe80b 100644
--- a/frontend/src/metabase/databases/components/DatabaseHostnameSectionField/DatabaseHostameSectionField.tsx
+++ b/frontend/src/metabase/databases/components/DatabaseHostnameSectionField/DatabaseHostameSectionField.tsx
@@ -22,7 +22,7 @@ const DatabaseHostnameSectionField = ({
   return (
     <FormField>
       <SectionButton type="button" onClick={handleClick}>
-        {value ? t`Use hostname` : t`Use  account name`}
+        {value ? t`Use account name` : t`Use hostname`}
       </SectionButton>
     </FormField>
   );
diff --git a/frontend/src/metabase/databases/constants.tsx b/frontend/src/metabase/databases/constants.tsx
index 89158205106c533ef0ea0d69edb176728ffd74f0..677c1af661d6aa82e6939d0e4d457740e5b9f538 100644
--- a/frontend/src/metabase/databases/constants.tsx
+++ b/frontend/src/metabase/databases/constants.tsx
@@ -3,6 +3,7 @@ import { t } from "ttag";
 import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase-lib/v1/metadata/utils/saved-questions";
 
 import DatabaseAuthCodeDescription from "./components/DatabaseAuthCodeDescription";
+import DatabaseAuthProviderSectionField from "./components/DatabaseAuthProviderSectionField";
 import DatabaseCacheScheduleField from "./components/DatabaseCacheScheduleField";
 import DatabaseClientIdDescription from "./components/DatabaseClientIdDescription";
 import DatabaseConnectionSectionField from "./components/DatabaseConnectionSectionField";
@@ -107,6 +108,9 @@ export const FIELD_OVERRIDES: Record<string, EngineFieldOverride> = {
   "use-hostname": {
     type: DatabaseHostnameSectionField,
   },
+  "use-auth-provider": {
+    type: DatabaseAuthProviderSectionField,
+  },
   "let-user-control-scheduling": {
     type: DatabaseScheduleToggleField,
   },
diff --git a/src/metabase/api/testing.clj b/src/metabase/api/testing.clj
index 5bca67863917e71ce0c31444c7407d25cb9e59ca..65bf6a4d76705416d1a970e8bb50aa8480cc5bbf 100644
--- a/src/metabase/api/testing.clj
+++ b/src/metabase/api/testing.clj
@@ -1,6 +1,7 @@
 (ns metabase.api.testing
   "Endpoints for testing."
   (:require
+   [cheshire.core :as json]
    [clojure.java.jdbc :as jdbc]
    [clojure.string :as str]
    [compojure.core :refer [POST]]
@@ -143,4 +144,15 @@
     {:result (if clock :set :reset)
      :time   (t/instant)}))
 
+(api/defendpoint GET "/echo"
+  "Simple echo hander. Fails when you GET {\"fail\": true}."
+  [fail body]
+  {fail ms/BooleanValue
+   body ms/JSONString}
+  (if fail
+    {:status 400
+     :body {:error-code "oops"}}
+    {:status 200
+     :body (json/decode body true)}))
+
 (api/define-routes)
diff --git a/src/metabase/auth_provider.clj b/src/metabase/auth_provider.clj
new file mode 100644
index 0000000000000000000000000000000000000000..320a71d76b83a33694684482ae7e4967043f22f2
--- /dev/null
+++ b/src/metabase/auth_provider.clj
@@ -0,0 +1,45 @@
+(ns metabase.auth-provider
+  (:require
+   [cheshire.core :as json]
+   [clj-http.client :as http]
+   [medley.core :as m]))
+
+(def azure-auth-token-renew-slack-seconds
+  "How many seconds before expiry we should prefer renewal.
+  This is a fairly arbitrary value, it's used just to avoid situations when we decide to use an
+  auth token which expires before we can put it to use."
+  60)
+
+(defmulti fetch-auth
+  "Multimethod for auth-provider implementations.
+   In general, implementations shouldn't change the shape of responses or names
+   so that [[driver/incorporate-auth-provider-details]] can decide how to incorporate into details."
+  (fn [auth-provider _database-id _db-details]
+    auth-provider))
+
+(defmethod fetch-auth :default
+  [_auth-provider _database-id _db-details]
+  nil)
+
+(defn- parse-http-headers [headers]
+  (json/parse-string headers))
+
+(defn- ^:dynamic *fetch-as-json* [url headers]
+  (let [headers (cond-> headers
+                  (string? headers) parse-http-headers)
+        response (http/get url (m/assoc-some {:as :json} :headers headers))]
+    (:body response)))
+
+(defmethod fetch-auth :http
+  [_ _database-id {:keys [http-auth-url http-auth-headers]}]
+  (*fetch-as-json* http-auth-url http-auth-headers))
+
+(defmethod fetch-auth :oauth
+  [_ _database-id {:keys [oauth-token-url oauth-token-headers]}]
+  (*fetch-as-json* oauth-token-url oauth-token-headers))
+
+(defmethod fetch-auth :azure-managed-identity
+  [_ _database-id {:keys [azure-managed-identity-client-id]}]
+  (*fetch-as-json* (str "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fossrdbms-aad.database.windows.net&client_id="
+                        azure-managed-identity-client-id)
+                   {"Metadata" "true"}))
diff --git a/src/metabase/db/data_source.clj b/src/metabase/db/data_source.clj
index 6b781adf5cce6c3a61fcfc1fa762c8bd5c0a636f..c8981a1dc763c95885db59a399fa95d5fe74fd89 100644
--- a/src/metabase/db/data_source.clj
+++ b/src/metabase/db/data_source.clj
@@ -4,6 +4,7 @@
   (:require
    [clojure.set :as set]
    [clojure.string :as str]
+   [metabase.auth-provider :as auth-provider]
    [metabase.config :as config]
    [metabase.connection-pool :as connection-pool]
    [metabase.db.spec :as mdb.spec]
@@ -17,6 +18,48 @@
 
 (set! *warn-on-reflection* true)
 
+(defn ^:dynamic *current-millis*
+  "Returns the current time millis, but can be overridden for testing."
+  []
+  (System/currentTimeMillis))
+
+(defn- renew-azure-managed-identity-password
+  [client-id]
+  (let [{:keys [access_token expires_in]}
+        (auth-provider/fetch-auth :azure-managed-identity nil {:azure-managed-identity-client-id client-id})]
+    {:password access_token
+     :expiry (+ (*current-millis*) (* (- (parse-long expires_in)
+                                         auth-provider/azure-auth-token-renew-slack-seconds)
+                                      1000))}))
+
+(defn- ensure-azure-managed-identity-password
+  "Make sure there is a \"password\" property in `properties` and returns a [[Properties]]
+  object without the azure managed identity properties.
+  Assumes that the \"azure-managed-identity-client-id\" property is only set if it
+  should be used to manage the password generation."
+  [^Properties properties]
+  (if-let [client-id (.getProperty properties "azure-managed-identity-client-id")]
+    (let [expiry (.get properties "password-expiry-timestamp")]
+      ;; check if we need to acquire the lock
+      (when (or (nil? (.getProperty properties "password"))
+                (nil? expiry)           ; should not happen, as expiry should be set when the password is set
+                (<= expiry (*current-millis*)))
+        (locking properties
+          ;; check if a new password has to be generated
+          (let [expiry (.get properties "password-expiry-timestamp")]
+            (when (or (nil? (.getProperty properties "password"))
+                      (nil? expiry)
+                      (<= expiry (*current-millis*)))
+              (let [{:keys [password expiry]} (renew-azure-managed-identity-password client-id)]
+                (doto properties
+                  (.setProperty "password" password)
+                  (.put "password-expiry-timestamp" expiry)))))))
+      (doto (Properties.)
+        (.putAll properties)
+        (.remove "azure-managed-identity-client-id")
+        (.remove "password-expiry-timestamp")))
+    properties))
+
 ;; NOTE: Never instantiate a DataSource directly
 ;; Use one of our helper functions below to ensure [[update-h2/update-if-needed!]] is called
 ;; You can use [[raw-connection-string->DataSource]] or [[broken-out-details->DataSource]]
@@ -32,7 +75,7 @@
   javax.sql.DataSource
   (getConnection [_]
     (doto (if properties
-            (DriverManager/getConnection url properties)
+            (DriverManager/getConnection url (ensure-azure-managed-identity-password properties))
             (DriverManager/getConnection url))
       ;; MySQL/MariaDB default to REPEATABLE_READ which ends up making everything SLOW because it locks all the time.
       ;; Postgres defaults to READ_COMMITTED. Explicitly set transaction isolation for new connections so we can make
@@ -58,9 +101,9 @@
 (defn raw-connection-string->DataSource
   "Return a [[javax.sql.DataSource]] given a raw JDBC connection string."
   (^javax.sql.DataSource [s]
-   (raw-connection-string->DataSource s nil nil))
+   (raw-connection-string->DataSource s nil nil nil))
 
-  (^javax.sql.DataSource [s username password]
+  (^javax.sql.DataSource [s username password azure-managed-identity-client-id]
    {:pre [(string? s)]}
    ;; normalize the protocol in case someone is trying to trip us up. Heroku is known for this and passes stuff in
    ;; like `postgres:...` to screw with us.
@@ -89,12 +132,26 @@
                  (log/error "Connection string contains a username, but MB_DB_USER is specified. MB_DB_USER will be used."))
          _     (when (and (:password m) (seq password))
                  (log/error "Connection string contains a password, but MB_DB_PASS is specified. MB_DB_PASS will be used."))
+         _     (when (and (seq password) (seq azure-managed-identity-client-id))
+                 (log/error "Both password and MB_DB_AZURE_MANAGED_IDENTITY_CLIENT_ID are specified. The password will be used."))
          m     (cond-> m
-                 (seq username) (assoc :user username)
-                 (seq password) (assoc :password password))]
+                 (seq username)                               (assoc :user username)
+                 (seq password)                               (assoc :password password)
+                 (and (empty? password)
+                      (seq azure-managed-identity-client-id)) (assoc :azure-managed-identity-client-id
+                                                                     azure-managed-identity-client-id))]
      (update-h2/update-if-needed! s)
      (->DataSource s (some-> (not-empty m) connection-pool/map->properties)))))
 
+(defn- remove-shadowed-azure-managed-identity-client-id
+  "A normal password takes precedence over Azure managed identity and we don't want
+  an empty string to be taken for a valid client ID."
+  [spec]
+  (cond-> spec
+    (or (empty? (:azure-managed-identity-client-id spec))
+        (seq (:password spec)))
+    (dissoc :azure-managed-identity-client-id)))
+
 (defn broken-out-details->DataSource
   "Return a [[javax.sql.DataSource]] given a broken-out Metabase connection details."
   ^javax.sql.DataSource [db-type details]
@@ -104,6 +161,7 @@
         _                                       (assert subname)
         url                                     (format "jdbc:%s:%s" subprotocol subname)
         properties                              (some-> (not-empty (dissoc spec :classname :subprotocol :subname))
+                                                        remove-shadowed-azure-managed-identity-client-id
                                                         connection-pool/map->properties)]
 
     (update-h2/update-if-needed! url)
diff --git a/src/metabase/db/env.clj b/src/metabase/db/env.clj
index 7d750aac6e6d9e0dcc26b109d3bf3fd1e3010da2..471641eb669f017e2207110794cae2513436740a 100644
--- a/src/metabase/db/env.clj
+++ b/src/metabase/db/env.clj
@@ -77,20 +77,23 @@
 (defn- broken-out-details
   "Connection details that can be used when pretending the Metabase DB is itself a `Database` (e.g., to use the Generic
   SQL driver functions on the Metabase DB itself)."
-  [db-type {:keys [mb-db-dbname mb-db-host mb-db-pass mb-db-port mb-db-user], :as env-vars}]
+  [db-type {:keys [mb-db-dbname mb-db-host mb-db-pass mb-db-port mb-db-user mb-db-azure-managed-identity-client-id]
+            :as   env-vars}]
   (if (= db-type :h2)
     (assoc h2-connection-properties
            :db (env->db-file env-vars))
-    {:host     mb-db-host
-     :port     mb-db-port
-     :db       mb-db-dbname
-     :user     mb-db-user
-     :password mb-db-pass}))
+    {:host                             mb-db-host
+     :port                             mb-db-port
+     :db                               mb-db-dbname
+     :user                             mb-db-user
+     :password                         mb-db-pass
+     :azure-managed-identity-client-id mb-db-azure-managed-identity-client-id}))
 
 (defn- env->DataSource
-  [db-type {:keys [mb-db-connection-uri mb-db-user mb-db-pass], :as env-vars}]
+  [db-type {:keys [mb-db-connection-uri mb-db-user mb-db-pass mb-db-azure-managed-identity-client-id], :as env-vars}]
   (if mb-db-connection-uri
-    (mdb.data-source/raw-connection-string->DataSource mb-db-connection-uri mb-db-user mb-db-pass)
+    (mdb.data-source/raw-connection-string->DataSource
+     mb-db-connection-uri mb-db-user mb-db-pass mb-db-azure-managed-identity-client-id)
     (mdb.data-source/broken-out-details->DataSource db-type (broken-out-details db-type env-vars))))
 
 
@@ -116,20 +119,21 @@
 
 (defn- env* [db-type]
   (merge-with
-   (fn [env-value default-value]
-     (if (nil? env-value)
-       default-value
-       env-value))
-   {:mb-db-type           db-type
-    :mb-db-in-memory      (config/config-bool :mb-db-in-memory)
-    :mb-db-file           (config/config-str :mb-db-file)
-    :mb-db-connection-uri (config/config-str :mb-db-connection-uri)
-    :mb-db-host           (config/config-str :mb-db-host)
-    :mb-db-port           (config/config-int :mb-db-port)
-    :mb-db-dbname         (config/config-str :mb-db-dbname)
-    :mb-db-user           (config/config-str :mb-db-user)
-    :mb-db-pass           (config/config-str :mb-db-pass)}
-   (env-defaults db-type)))
+    (fn [env-value default-value]
+      (if (nil? env-value)
+        default-value
+        env-value))
+    {:mb-db-type                             db-type
+     :mb-db-in-memory                        (config/config-bool :mb-db-in-memory)
+     :mb-db-file                             (config/config-str :mb-db-file)
+     :mb-db-connection-uri                   (config/config-str :mb-db-connection-uri)
+     :mb-db-host                             (config/config-str :mb-db-host)
+     :mb-db-port                             (config/config-int :mb-db-port)
+     :mb-db-dbname                           (config/config-str :mb-db-dbname)
+     :mb-db-user                             (config/config-str :mb-db-user)
+     :mb-db-pass                             (config/config-str :mb-db-pass)
+     :mb-db-azure-managed-identity-client-id (config/config-str :mb-db-azure-managed-identity-client-id)}
+    (env-defaults db-type)))
 
 (def env
   "Metabase Datatbase environment. Used to setup *application-db* and audit-db for enterprise users."
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 586516c205b7d5f151c9f16fe7352e8d0dcfb8fc..d0270d693d68543cec41825fbd71b96aa9bb11c2 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -10,6 +10,7 @@
    [clojure.set :as set]
    [clojure.string :as str]
    [java-time.api :as t]
+   [metabase.auth-provider :as auth-provider]
    [metabase.driver.impl :as driver.impl]
    [metabase.models.setting :as setting :refer [defsetting]]
    [metabase.plugins.classloader :as classloader]
@@ -966,6 +967,30 @@
   dispatch-on-uninitialized-driver
   :hierarchy #'hierarchy)
 
+(defmulti incorporate-auth-provider-details
+  "A multimethod for driver specific behavior required to incorporate response of an auth-provider into the DB details.
+   In most cases this means setting the :password and/or :username based on the auth-provider and its response."
+  {:added "0.50.17" :arglists '([driver auth-provider auth-provider-response details])}
+  dispatch-on-initialized-driver
+  :hierarchy #'hierarchy)
+
+(defmethod incorporate-auth-provider-details :default
+  [_driver _auth-provider _auth-provider-response details]
+  details)
+
+(defmethod incorporate-auth-provider-details :sql-jdbc
+  [_driver auth-provider auth-provider-response details]
+  (case auth-provider
+    (:oauth :azure-managed-identity)
+    (let [{:keys [access_token expires_in]} auth-provider-response]
+      (cond-> (assoc details :password access_token)
+        expires_in (assoc :password-expiry-timestamp (+ (System/currentTimeMillis)
+                                                        (* (- (parse-long expires_in)
+                                                              auth-provider/azure-auth-token-renew-slack-seconds)
+                                                           1000)))))
+
+    (merge details auth-provider-response)))
+
 ;;; TODO:
 ;;;
 ;;; 1. We definitely should not be asking drivers to "update the value for `:details`". Drivers shouldn't touch the
@@ -983,9 +1008,9 @@
   :hierarchy #'hierarchy)
 
 (defmethod normalize-db-details ::driver
-  [_ db-details]
+  [_ database]
   ;; no normalization by default
-  db-details)
+  database)
 
 (defmulti superseded-by
   "Returns the driver that supersedes the given `driver`.  A non-nil return value means that the given `driver` is
diff --git a/src/metabase/driver/common.clj b/src/metabase/driver/common.clj
index 2f8724cfa518dc926bf1e4dc2cc124671fe9e5d8..58e2bb9f6ed576fb4495b4148d141b75f159515e 100644
--- a/src/metabase/driver/common.clj
+++ b/src/metabase/driver/common.clj
@@ -226,6 +226,32 @@
   added to the plugin manifest as connection properties, similar to the keys in the `default-options` map."
   {:cloud-ip-address-info cloud-ip-address-info})
 
+(def auth-provider-options
+  "Options for using an auth provider instead of a literal password."
+  [{:name "use-auth-provider"
+    :type :section
+    :default false}
+   {:name "auth-provider"
+    :display-name (deferred-tru "Auth provider")
+    :type :select
+    :options [{:name (deferred-tru "Azure Managed Identity")
+               :value "azure-managed-identity"}
+              {:name (deferred-tru "OAuth")
+               :value "oauth"}]
+    :default "azure-managed-identity"
+    :visible-if {"use-auth-provider" true}}
+   {:name "azure-managed-identity-client-id"
+    :display-name (deferred-tru "Client ID")
+    :required true
+    :visible-if {"auth-provider" "azure-managed-identity"}}
+   {:name "oauth-token-url"
+    :display-name (deferred-tru "Auth token URL")
+    :required true
+    :visible-if {"auth-provider" "oauth"}}
+   {:name "oauth-token-headers"
+    :display-name (deferred-tru "Auth token request headers (a JSON map)")
+    :visible-if {"auth-provider" "oauth"}}])
+
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                               Class -> Base Type                                               |
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index c08d8d424a3bd0d6e03039b08cbcade2553a64a9..138a6a81612b228fd0317d196985395130fa63f0 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -136,7 +136,9 @@
     (assoc driver.common/default-port-details :placeholder 5432)
     driver.common/default-dbname-details
     driver.common/default-user-details
-    driver.common/default-password-details
+    driver.common/auth-provider-options
+    (assoc driver.common/default-password-details
+           :visible-if {"use-auth-provider" false})
     driver.common/cloud-ip-address-info
     {:name "schema-filters"
      :type :schema-filters
diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj
index 84c34016986a6871ab52c18e17ea7c42a5a8adfe..f8b59711353af687b55f926e7f95043481a5c94a 100644
--- a/src/metabase/driver/sql_jdbc/connection.clj
+++ b/src/metabase/driver/sql_jdbc/connection.clj
@@ -6,6 +6,7 @@
    [metabase.connection-pool :as connection-pool]
    [metabase.db :as mdb]
    [metabase.driver :as driver]
+   [metabase.driver.util :as driver.u]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.jvm :as lib.metadata.jvm]
    [metabase.models.interface :as mi]
@@ -170,12 +171,18 @@ For setting the maximum, see [MB_APPLICATION_DB_MAX_CONNECTION_POOL_SIZE](#mb_ap
   (let [details-with-tunnel (driver/incorporate-ssh-tunnel-details  ;; If the tunnel is disabled this returned unchanged
                              driver
                              (update details :port #(or % (default-ssh-tunnel-target-port driver))))
-        spec                (connection-details->spec driver details-with-tunnel)
+        details-with-auth   (driver.u/fetch-and-incorporate-auth-provider-details
+                             driver
+                             id
+                             details-with-tunnel)
+        spec                (connection-details->spec driver details-with-auth)
         properties          (data-warehouse-connection-pool-properties driver database)]
     (merge
-      (connection-pool-spec spec properties)
-      ;; also capture entries related to ssh tunneling for later use
-      (select-keys spec [:tunnel-enabled :tunnel-session :tunnel-tracker :tunnel-entrance-port :tunnel-entrance-host]))))
+     (connection-pool-spec spec properties)
+     ;; also capture entries related to ssh tunneling for later use
+     (select-keys spec [:tunnel-enabled :tunnel-session :tunnel-tracker :tunnel-entrance-port :tunnel-entrance-host])
+     ;; remember when the password expires
+     (select-keys details-with-auth [:password-expiry-timestamp]))))
 
 (defn- destroy-pool! [database-id pool-spec]
   (log/debug (u/format-color :red "Closing old connection pool for database %s ..." database-id))
@@ -228,6 +235,10 @@ For setting the maximum, see [MB_APPLICATION_DB_MAX_CONNECTION_POOL_SIZE](#mb_ap
   (log/warn (u/format-color :yellow "Hash of database %s details changed; marking pool invalid to reopen it" db-id))
   nil)
 
+(defn- log-password-expiry! [db-id]
+  (log/warn (u/format-color :yellow "Password of database %s expired; marking pool invalid to reopen it" db-id))
+  nil)
+
 (defn db->pooled-connection-spec
   "Return a JDBC connection spec that includes a cp30 `ComboPooledDataSource`. These connection pools are cached so we
   don't create multiple ones for the same DB."
@@ -267,6 +278,12 @@ For setting the maximum, see [MB_APPLICATION_DB_MAX_CONNECTION_POOL_SIZE](#mb_ap
                             (when log-invalidation?
                               (log-jdbc-spec-hash-change-msg! db-id))
 
+                            (let [{:keys [password-expiry-timestamp]} details]
+                              (and (int? password-expiry-timestamp)
+                                   (<= password-expiry-timestamp (System/currentTimeMillis))))
+                            (when log-invalidation?
+                              (log-password-expiry! db-id))
+
                             (nil? (:tunnel-session details)) ; no tunnel in use; valid
                             details
 
@@ -310,7 +327,10 @@ For setting the maximum, see [MB_APPLICATION_DB_MAX_CONNECTION_POOL_SIZE](#mb_ap
   [driver details f]
   (let [details (update details :port #(or % (default-ssh-tunnel-target-port driver)))]
     (ssh/with-ssh-tunnel [details-with-tunnel details]
-      (let [spec (connection-details->spec driver details-with-tunnel)]
+      (let [details-with-auth (driver.u/fetch-and-incorporate-auth-provider-details
+                               driver
+                               details-with-tunnel)
+            spec (connection-details->spec driver details-with-auth)]
         (f spec)))))
 
 (defmacro with-connection-spec-for-testing-connection
diff --git a/src/metabase/driver/util.clj b/src/metabase/driver/util.clj
index bbfa7261b86fcd43d92d7aee686f320858c845ec..9880f1ba9b128f44e0db5bf1237c09faa1a0161f 100644
--- a/src/metabase/driver/util.clj
+++ b/src/metabase/driver/util.clj
@@ -4,6 +4,7 @@
    [clojure.core.memoize :as memoize]
    [clojure.set :as set]
    [clojure.string :as str]
+   [metabase.auth-provider :as auth-provider]
    [metabase.config :as config]
    [metabase.db :as mdb]
    [metabase.driver :as driver]
@@ -659,3 +660,19 @@
           password-fields (filter #(contains? #{:password :secret} (get % :type)) all-fields)]
       (into default-sensitive-fields (map (comp keyword :name) password-fields)))
     default-sensitive-fields))
+
+(defn fetch-and-incorporate-auth-provider-details
+  "Incorporates auth-provider responses with db-details.
+
+  If you have a database you need to pass the database-id as some providers will need to save the response (e.g. refresh-tokens)."
+  ([driver db-details]
+   (fetch-and-incorporate-auth-provider-details driver nil db-details))
+  ([driver database-id {:keys [use-auth-provider auth-provider] :as db-details}]
+   (if use-auth-provider
+     (let [auth-provider (keyword auth-provider)]
+       (driver/incorporate-auth-provider-details
+        driver
+        auth-provider
+        (auth-provider/fetch-auth auth-provider database-id db-details)
+        db-details))
+     db-details)))
diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index e3f77547bd38a0ab1ed961fcac089d116acc8c15..f63e16c9d2e935acb0109f27779295eb976fbc57 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -173,14 +173,16 @@
   [{driver :engine, :as database}]
   (letfn [(normalize-details [db]
             (binding [*normalizing-details* true]
-              (driver/normalize-db-details driver db)))]
+              (driver/normalize-db-details
+               driver
+               (m/update-existing-in db [:details :auth-provider] keyword))))]
     (cond-> database
       ;; TODO - this is only really needed for API responses. This should be a `hydrate` thing instead!
       (driver.impl/registered? driver)
       (assoc :features (driver.u/features driver (t2.realize/realize database)))
 
       (and (driver.impl/registered? driver)
-           (:details database)
+           (map? (:details database))
            (not *normalizing-details*))
       normalize-details)))
 
diff --git a/test/metabase/auth_provider_test.clj b/test/metabase/auth_provider_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..2a09f3c5537b86e4c9826f57e4fbc77cf507452a
--- /dev/null
+++ b/test/metabase/auth_provider_test.clj
@@ -0,0 +1,30 @@
+(ns metabase.auth-provider-test
+  (:require
+   [cheshire.core :as json]
+   [clojure.test :refer [deftest is testing]]
+   [metabase.api.database :as api.database]
+   [metabase.http-client :as client]
+   [metabase.sync :as sync]
+   [metabase.test :as mt]
+   [metabase.test.data.interface :as tx]))
+
+(deftest auth-integration-test
+  (mt/test-drivers #{:postgres :mysql}
+    (let [original-details (:details (mt/db))
+          auth-details {:use-auth-provider true
+                        :auth-provider :http
+                        :http-auth-url (client/build-url "/testing/echo"
+                                                         {:body (json/encode original-details)})}]
+      (mt/with-temp [:model/Database db
+                     {:engine (tx/driver),
+                      :details auth-details}]
+        (mt/with-db
+          db
+          (is (= auth-details (:details (mt/db))))
+          (testing "Connection tests"
+            (is (nil? (api.database/test-database-connection (:engine db) (:details db)))))
+          (testing "Syncing does not blow up"
+            (sync/sync-database! (mt/db)))
+          (testing "Querying"
+            (is (= [["Polo Lounge"]]
+                   (mt/rows (mt/run-mbql-query venues {:filter [:= $id 60] :fields [$name]}))))))))))
diff --git a/test/metabase/db/data_source_test.clj b/test/metabase/db/data_source_test.clj
index 7a54dccb2ba84344dcb69a63c8ef2d3b9e6ae9b5..e38562f97d82a2a0020394f3d8af0c33ac4a94e8 100644
--- a/test/metabase/db/data_source_test.clj
+++ b/test/metabase/db/data_source_test.clj
@@ -2,10 +2,13 @@
   (:require
    [clojure.java.jdbc :as jdbc]
    [clojure.test :refer :all]
+   [metabase.auth-provider :as auth-provider]
    [metabase.config :as config]
    [metabase.connection-pool :as connection-pool]
    [metabase.db.data-source :as mdb.data-source]
-   [metabase.test :as mt]))
+   [metabase.test :as mt])
+  (:import
+   (java.util Properties)))
 
 (set! *warn-on-reflection* true)
 
@@ -26,6 +29,32 @@
                                                                       :password "1234"
                                                                       :db       "metabase"}))))
 
+  (testing :azure-managed-identity
+    (is (= (->DataSource
+            "jdbc:postgresql://localhost:5432/metabase"
+            {"ApplicationName"                  config/mb-version-and-process-identifier
+             "OpenSourceSubProtocolOverride"    "true"
+             "user"                             "cam"
+             "azure-managed-identity-client-id" "client ID"})
+           (mdb.data-source/broken-out-details->DataSource :postgres {:host                             "localhost"
+                                                                      :port                             5432
+                                                                      :user                             "cam"
+                                                                      :azure-managed-identity-client-id "client ID"
+                                                                      :db                               "metabase"})))
+    (testing "password takes precedence"
+      (is (= (->DataSource
+              "jdbc:postgresql://localhost:5432/metabase"
+              {"password"                      "1234"
+               "ApplicationName"               config/mb-version-and-process-identifier
+               "OpenSourceSubProtocolOverride" "true"
+               "user"                          "cam"})
+             (mdb.data-source/broken-out-details->DataSource :postgres {:host                             "localhost"
+                                                                        :port                             5432
+                                                                        :user                             "cam"
+                                                                        :password                         "1234"
+                                                                        :azure-managed-identity-client-id "client ID"
+                                                                        :db                               "metabase"})))))
+
   (testing :h2
     (is (= (->DataSource
             "jdbc:h2:file:/metabase/metabase.db"
@@ -102,19 +131,28 @@
       (is (= (->DataSource
               "jdbc:postgresql://metabase"
               {"user" "cam", "password" "1234"})
-             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" "1234"))))
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" "1234" nil)
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" "1234" "client ID"))))
     (testing "username only"
       (is (= (->DataSource
               "jdbc:postgresql://metabase"
               {"user" "cam"})
-             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" nil)
-             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" ""))))
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" nil nil)
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" "" nil))))
+    (testing "username and azure-managed-identity-client-id"
+      (is (= (->DataSource
+              "jdbc:postgresql://metabase"
+              {"user" "cam"
+               "azure-managed-identity-client-id" "client ID"})
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" nil "client ID")
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "cam" "" "client ID"))))
     (testing "password only"
       (is (= (->DataSource
               "jdbc:postgresql://metabase"
               {"password" "1234"})
-             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" nil "1234")
-             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "" "1234"))))))
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" nil "1234" nil)
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "" "1234" nil)
+             (mdb.data-source/raw-connection-string->DataSource "postgres://metabase" "" "1234" "client ID"))))))
 
 (deftest ^:parallel equality-test
   (testing "Two DataSources with the same URL should be equal"
@@ -124,3 +162,60 @@
   (testing "Two DataSources with the same URL and properties should be equal"
     (is (= (mdb.data-source/broken-out-details->DataSource :h2 {:db "wow", :x 1})
            (mdb.data-source/broken-out-details->DataSource :h2 {:db "wow", :x 1})))))
+
+(deftest ^:parallel ensure-azure-managed-identity-password-test
+  (testing "nothing happens if ensure-azure-managed-identity-client-id is missing"
+    (let [props (Properties.)]
+      (is (empty? (#'mdb.data-source/ensure-azure-managed-identity-password props)))
+      (is (empty? props))))
+  (testing "password is set if it's missing"
+    (let [now 0
+          expiry-secs 1000
+          expiry (+ now (* (- expiry-secs auth-provider/azure-auth-token-renew-slack-seconds) 1000))
+          props (doto (Properties.)
+                  (.setProperty "azure-managed-identity-client-id" "client ID"))]
+      (binding [auth-provider/*fetch-as-json* (fn [_url _headers]
+                                                {:access_token "access token"
+                                                 :expires_in (str expiry-secs)})
+                mdb.data-source/*current-millis* (constantly now)]
+        (is (= {"password" "access token"}
+               (#'mdb.data-source/ensure-azure-managed-identity-password props))))
+      (is (= {"azure-managed-identity-client-id" "client ID"
+              "password" "access token"
+              "password-expiry-timestamp" expiry}
+             props))))
+  (testing "nothing happens if a fresh enough password is present"
+    (let [now 0
+          expiry-secs 1000
+          expiry (+ now (* (- expiry-secs auth-provider/azure-auth-token-renew-slack-seconds) 1000))
+          props (doto (Properties.)
+                  (.putAll {"azure-managed-identity-client-id" "client ID"
+                            "password" "access token"
+                            "password-expiry-timestamp" expiry}))]
+      (binding [auth-provider/*fetch-as-json* (fn [_url _headers]
+                                                (is false "should not get called"))
+                mdb.data-source/*current-millis* (constantly now)]
+        (is (= {"password" "access token"}
+               (#'mdb.data-source/ensure-azure-managed-identity-password props))))
+      (is (= {"azure-managed-identity-client-id" "client ID"
+              "password" "access token"
+              "password-expiry-timestamp" expiry}
+             props))))
+  (testing "a new password is set if the old one is stale"
+    (let [now 0
+          expiry-secs 1000
+          expiry (+ now (* (- expiry-secs auth-provider/azure-auth-token-renew-slack-seconds) 1000))
+          props (doto (Properties.)
+                  (.putAll {"azure-managed-identity-client-id" "client ID"
+                            "password" "access token"
+                            "password-expiry-timestamp" 0}))]
+      (binding [auth-provider/*fetch-as-json* (fn [_url _headers]
+                                                {:access_token "new access token"
+                                                 :expires_in (str expiry-secs)})
+                mdb.data-source/*current-millis* (constantly now)]
+        (is (= {"password" "new access token"}
+               (#'mdb.data-source/ensure-azure-managed-identity-password props))))
+      (is (= {"azure-managed-identity-client-id" "client ID"
+              "password" "new access token"
+              "password-expiry-timestamp" expiry}
+             props)))))
diff --git a/test/metabase/db/env_test.clj b/test/metabase/db/env_test.clj
index 0989c6a4e5010fc894d8bba080ffce68567ed8c9..f16a0b5a3188f9d3a32734dc4010687c051120db 100644
--- a/test/metabase/db/env_test.clj
+++ b/test/metabase/db/env_test.clj
@@ -19,14 +19,14 @@
 
   (testing "Raw connection string should support separate username and/or password (#20122)"
     (testing "username and password"
-      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" "cam" "1234")
+      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" "cam" "1234" nil)
              (#'mdb.env/env->DataSource :postgres {:mb-db-connection-uri "postgres://metabase", :mb-db-user "cam", :mb-db-pass "1234"}))))
     (testing "username only"
-      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" "cam" nil)
+      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" "cam" nil nil)
              (#'mdb.env/env->DataSource :postgres {:mb-db-connection-uri "postgres://metabase", :mb-db-user "cam"})
              (#'mdb.env/env->DataSource :postgres {:mb-db-connection-uri "postgres://metabase", :mb-db-user "cam", :mb-db-pass ""}))))
     (testing "password only"
-      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" nil "1234")
+      (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:postgresql://metabase" nil "1234" nil)
              (#'mdb.env/env->DataSource :postgres {:mb-db-connection-uri "postgres://metabase", :mb-db-pass "1234"})
              (#'mdb.env/env->DataSource :postgres {:mb-db-connection-uri "postgres://metabase", :mb-db-user  "", :mb-db-pass "1234"}))))))
 
diff --git a/test/metabase/driver/sql_jdbc/connection_test.clj b/test/metabase/driver/sql_jdbc/connection_test.clj
index c286b45da2bd16d3c21871f7c4f2e1b36ea0f091..109f97bac84693d2db4297280a4b25b382c6a4b1 100644
--- a/test/metabase/driver/sql_jdbc/connection_test.clj
+++ b/test/metabase/driver/sql_jdbc/connection_test.clj
@@ -3,6 +3,7 @@
    [clojure.java.jdbc :as jdbc]
    [clojure.string :as str]
    [clojure.test :refer :all]
+   [metabase.auth-provider :as auth-provider]
    [metabase.config :as config]
    [metabase.core :as mbc]
    [metabase.db :as mdb]
@@ -230,6 +231,39 @@
         server (Server/createTcpServer (into-array args))]
     (doto server (.start))))
 
+(deftest test-auth-provider-connection
+  (mt/test-drivers #{:postgres}
+    (testing "Azure Managed Identity connections can be created and expired passwords get renewed"
+      (let [db-details (:details (mt/db))
+            oauth-db-details (-> db-details
+                                 (dissoc :password)
+                                 (assoc :use-auth-provider true
+                                        :auth-provider :azure-managed-identity
+                                        :azure-managed-identity-client-id "client ID"))
+            ;; we return an expired token which forces a renewal when a second connection is requested
+            ;; (the first time it is used without checking for expiry)
+            expires-in (atom "0")
+            connection-creations (atom 0)]
+        (binding [auth-provider/*fetch-as-json* (fn [url _headers]
+                                                  (is (str/includes? url "client ID"))
+                                                  (swap! connection-creations inc)
+                                                  {:access_token (:password db-details)
+                                                   :expires_in @expires-in})]
+          (t2.with-temp/with-temp [Database oauth-db {:engine (tx/driver), :details oauth-db-details}]
+            (mt/with-db oauth-db
+              (try
+                ;; since Metabase is running and using the pool of this DB, the sync might fail
+                ;; if the connection pool is shut down during the sync
+                (sync/sync-database! (mt/db))
+                (catch Exception _))
+              ;; after "fixing" the expiry, we should get a connection from a pool that doesn't get shut down
+              (reset! expires-in "10000")
+              (sync/sync-database! (mt/db))
+              (is (= [["Polo Lounge"]]
+                     (mt/rows (mt/run-mbql-query venues {:filter [:= $id 60] :fields [$name]}))))
+              ;; we must have created more than one connection
+              (is (> @connection-creations 1)))))))))
+
 (deftest test-ssh-tunnel-connection
   ;; TODO: Formerly this test ran against "all JDBC drivers except this big list":
   ;; (apply disj (sql-jdbc.tu/sql-jdbc-drivers)
diff --git a/test/metabase/driver/util_test.clj b/test/metabase/driver/util_test.clj
index 41a4faa0d678c49a1d9685ee6112e7608f13c393..fa4f3cfb500d3454745a0e66144e77e413900729 100644
--- a/test/metabase/driver/util_test.clj
+++ b/test/metabase/driver/util_test.clj
@@ -1,15 +1,19 @@
 (ns metabase.driver.util-test
   (:require
+   [cheshire.core :as json]
    [clojure.java.io :as io]
    [clojure.string :as str]
    [clojure.test :refer :all]
+   [metabase.auth-provider :as auth-provider]
    [metabase.driver :as driver]
    [metabase.driver.h2 :as h2]
    [metabase.driver.util :as driver.u]
+   [metabase.http-client :as client]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]
    [metabase.query-processor.store :as qp.store]
    [metabase.test :as mt]
+   [metabase.test.data.interface :as tx]
    [metabase.test.fixtures :as fixtures]
    [metabase.util :as u])
   (:import
@@ -312,3 +316,77 @@
             (let [log-messages (mt/with-log-messages-for-level [metabase.driver.util :error]
                                  (is (false? (driver.u/supports? :test-driver :expressions db))))]
               (is (nil? log-messages)))))))))
+
+(defmethod auth-provider/fetch-auth ::test-me
+  [_provider _db-id details]
+  (is (= "testing" (:key details)))
+  {:password "qux"})
+
+(deftest ^:parallel simple-test
+  (let [details {:username "test"
+                 :password "ignored"
+                 :use-auth-provider true
+                 :auth-provider ::test-me
+                 :key "testing"}]
+    (mt/with-temp [:model/Database db {:details details}]
+      (is (=? (assoc details :password "qux")
+              (driver.u/fetch-and-incorporate-auth-provider-details
+               (:engine db)
+               (:id db)
+               (:details db)))))))
+
+(deftest http-provider-tests
+  (let [original-details (:details (mt/db))
+        provider-details {:use-auth-provider true
+                          :auth-provider "http"
+                          :http-auth-url (client/build-url "/testing/echo"
+                                                           {:body (json/encode original-details)})}]
+    (is (= original-details (auth-provider/fetch-auth :http nil provider-details)))
+    (is (= (merge provider-details original-details)
+           (driver.u/fetch-and-incorporate-auth-provider-details
+            (tx/driver)
+            provider-details)))))
+
+(deftest oauth-provider-tests
+  (let [oauth-response {:access_token "foobar"
+                        :expires_in "84791"}
+        provider-details {:use-auth-provider true
+                          :auth-provider :oauth
+                          :oauth-token-url (client/build-url "/testing/echo"
+                                                             {:body (json/encode oauth-response)})}]
+    (is (= oauth-response (auth-provider/fetch-auth :oauth nil provider-details)))
+    (is (=? (merge provider-details
+                   {:password "foobar"
+                    :password-expiry-timestamp #(and (int? %) (> % (System/currentTimeMillis)))})
+            (driver.u/fetch-and-incorporate-auth-provider-details
+             (tx/driver)
+             provider-details)))))
+
+(deftest ^:parallel azure-managed-identity-provider-tests
+  (testing "password gets resolved"
+    (let [client-id "client ID"
+          provider-details {:use-auth-provider true
+                            :auth-provider :azure-managed-identity
+                            :azure-managed-identity-client-id client-id
+                            :password "xyz"}
+          response-body {:access_token "foobar"}]
+      (binding [auth-provider/*fetch-as-json* (fn [url _headers]
+                                                (is (str/includes? url client-id))
+                                                response-body)]
+        (is (= response-body (auth-provider/fetch-auth :azure-managed-identity nil provider-details)))
+        (is (= (merge provider-details {:password "foobar"})
+               (driver.u/fetch-and-incorporate-auth-provider-details
+                (tx/driver)
+                provider-details))))))
+  (testing "existing password doesn't get overwritten if not using an auth provider"
+    (let [client-id "client ID"
+          provider-details {:use-auth-provider false
+                            :auth-provider :azure-managed-identity
+                            :azure-managed-identity-client-id client-id
+                            :password "xyz"}]
+      (binding [auth-provider/*fetch-as-json* (fn [_url _headers]
+                                                (is false "should not get called"))]
+        (is (= provider-details
+               (driver.u/fetch-and-incorporate-auth-provider-details
+                (tx/driver)
+                provider-details)))))))