Merge pull request #398 from EandrewJones/#397-Add-auth-header-callback-option

#397 Add authHeader callback option 
diff --git a/build/UserALEWebExtension/background.js b/build/UserALEWebExtension/background.js
index 126c474..845cad6 100644
--- a/build/UserALEWebExtension/background.js
+++ b/build/UserALEWebExtension/background.js
@@ -347,9 +347,9 @@
  * The ASF licenses this file to You 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.
@@ -397,8 +397,13 @@
  * @param  {Object} config Configuration object to be read from.
  */
 function sendOnClose(logs, config) {
-  window.addEventListener('pagehide', function () {
+  window.addEventListener("pagehide", function () {
     if (config.on && logs.length > 0) {
+      // NOTE: sendBeacon does not support auth headers,
+      // so this will fail if auth is required.
+      // The alternative is to use fetch() with keepalive: true
+      // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description
+      // https://stackoverflow.com/a/73062712/9263449
       navigator.sendBeacon(config.url, JSON.stringify(logs));
       logs.splice(0); // clear log queue
     }
@@ -416,14 +421,12 @@
 // @todo expose config object to sendLogs replate url with config.url
 function sendLogs(logs, config, retries) {
   var req = new XMLHttpRequest();
-
-  // @todo setRequestHeader for Auth
   var data = JSON.stringify(logs);
-  req.open('POST', config.url);
+  req.open("POST", config.url);
   if (config.authHeader) {
-    req.setRequestHeader('Authorization', config.authHeader);
+    req.setRequestHeader("Authorization", config.authHeader);
   }
-  req.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
+  req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
   req.onreadystatechange = function () {
     if (req.readyState === 4 && req.status !== 200) {
       if (retries > 0) {
diff --git a/build/UserALEWebExtension/content.js b/build/UserALEWebExtension/content.js
index 2ec7e1f..d3dbf9b 100644
--- a/build/UserALEWebExtension/content.js
+++ b/build/UserALEWebExtension/content.js
@@ -913,9 +913,9 @@
  * The ASF licenses this file to You 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.
@@ -963,8 +963,13 @@
  * @param  {Object} config Configuration object to be read from.
  */
 function sendOnClose(logs, config) {
-  window.addEventListener('pagehide', function () {
+  window.addEventListener("pagehide", function () {
     if (config.on && logs.length > 0) {
+      // NOTE: sendBeacon does not support auth headers,
+      // so this will fail if auth is required.
+      // The alternative is to use fetch() with keepalive: true
+      // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description
+      // https://stackoverflow.com/a/73062712/9263449
       navigator.sendBeacon(config.url, JSON.stringify(logs));
       logs.splice(0); // clear log queue
     }
@@ -982,14 +987,12 @@
 // @todo expose config object to sendLogs replate url with config.url
 function sendLogs(logs, config, retries) {
   var req = new XMLHttpRequest();
-
-  // @todo setRequestHeader for Auth
   var data = JSON.stringify(logs);
-  req.open('POST', config.url);
+  req.open("POST", config.url);
   if (config.authHeader) {
-    req.setRequestHeader('Authorization', config.authHeader);
+    req.setRequestHeader("Authorization", config.authHeader);
   }
-  req.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
+  req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
   req.onreadystatechange = function () {
     if (req.readyState === 4 && req.status !== 200) {
       if (retries > 0) {
diff --git a/build/userale-2.4.0.js b/build/userale-2.4.0.js
index 6b89cc4..4876a80 100644
--- a/build/userale-2.4.0.js
+++ b/build/userale-2.4.0.js
@@ -975,9 +975,79 @@
    * The ASF licenses this file to You 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.
+   */
+
+  var authCallback = null;
+
+  /**
+   * Fetches the most up-to-date auth header string from the auth callback
+   * and updates the config object with the new value.
+   * @param {Object} config Configuration object to be updated.
+   * @param {Function} authCallback Callback used to fetch the newest header.
+   * @returns {void}
+   */
+  function updateAuthHeader(config) {
+    if (authCallback) {
+      try {
+        config.authHeader = authCallback();
+      } catch (e) {
+        // We should emit the error, but otherwise continue as this could be a temporary issue
+        // due to network connectivity or some logic inside the authCallback which is the user's
+        // responsibility.
+        console.error("Error encountered while setting the auth header: ".concat(e));
+      }
+    }
+  }
+
+  /**
+   * Registers the provided callback to be used when updating the auth header.
+   * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+   * @returns {boolean} Whether the operation succeeded.
+   */
+  function registerAuthCallback(callback) {
+    try {
+      verifyCallback(callback);
+      authCallback = callback;
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  /**
+   * Verify that the provided callback is a function which returns a string
+   * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+   * @throws {Error} If the callback is not a function or does not return a string.
+   * @returns {void}
+   */
+  function verifyCallback(callback) {
+    if (typeof callback !== "function") {
+      throw new Error("Userale auth callback must be a function");
+    }
+    var result = callback();
+    if (typeof result !== "string") {
+      throw new Error("Userale auth callback must return a string");
+    }
+  }
+
+  /*
+   * Licensed to the Apache Software Foundation (ASF) under one or more
+   * contributor license agreements.  See the NOTICE file distributed with
+   * this work for additional information regarding copyright ownership.
+   * The ASF licenses this file to You 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.
@@ -1025,8 +1095,13 @@
    * @param  {Object} config Configuration object to be read from.
    */
   function sendOnClose(logs, config) {
-    window.addEventListener('pagehide', function () {
+    window.addEventListener("pagehide", function () {
       if (config.on && logs.length > 0) {
+        // NOTE: sendBeacon does not support auth headers,
+        // so this will fail if auth is required.
+        // The alternative is to use fetch() with keepalive: true
+        // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description
+        // https://stackoverflow.com/a/73062712/9263449
         navigator.sendBeacon(config.url, JSON.stringify(logs));
         logs.splice(0); // clear log queue
       }
@@ -1044,14 +1119,15 @@
   // @todo expose config object to sendLogs replate url with config.url
   function sendLogs(logs, config, retries) {
     var req = new XMLHttpRequest();
-
-    // @todo setRequestHeader for Auth
     var data = JSON.stringify(logs);
-    req.open('POST', config.url);
+    req.open("POST", config.url);
+
+    // Update headers
+    updateAuthHeader(config);
     if (config.authHeader) {
-      req.setRequestHeader('Authorization', config.authHeader);
+      req.setRequestHeader("Authorization", config.authHeader);
     }
-    req.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
+    req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
     req.onreadystatechange = function () {
       if (req.readyState === 4 && req.status !== 200) {
         if (retries > 0) {
@@ -1164,6 +1240,7 @@
   exports.options = options;
   exports.packageCustomLog = packageCustomLog;
   exports.packageLog = packageLog;
+  exports.registerAuthCallback = registerAuthCallback;
   exports.removeCallbacks = removeCallbacks;
   exports.start = start;
   exports.stop = stop;
diff --git a/build/userale-2.4.0.min.js b/build/userale-2.4.0.min.js
index 81d0c30..6d4277b 100644
--- a/build/userale-2.4.0.min.js
+++ b/build/userale-2.4.0.min.js
@@ -15,4 +15,4 @@
  * limitations under the License.
  * @preserved
  */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).userale={})}(this,(function(e){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t(e)}var n="2.4.0",o=null;function r(e,t){var n=e.autostart,o=t.autostart;Object.keys(t).forEach((function(n){if("userFromParams"===n){var o=(r=t[n],i=new RegExp("[?&]"+r+"(=([^&#]*)|&|#|$)"),(a=window.location.href.match(i))&&a[2]?decodeURIComponent(a[2].replace(/\+/g," ")):null);o&&(e.userId=o)}var r,i,a;e[n]=t[n]})),!1!==n&&!1!==o||(e.autostart=!1)}var i=function(e,t,n){if(n||2===arguments.length)for(var o,r=0,i=t.length;r<i;r++)!o&&r in t||(o||(o=Array.prototype.slice.call(t,0,r)),o[r]=t[r]);return e.concat(o||Array.prototype.slice.call(t))},a=function(e,t,n){this.name=e,this.version=t,this.os=n,this.type="browser"},s=function(e){this.version=e,this.type="node",this.name="node",this.os=process.platform},l=function(e,t,n,o){this.name=e,this.version=t,this.os=n,this.bot=o,this.type="bot-device"},u=function(){this.type="bot",this.bot=!0,this.name="bot",this.version=null,this.os=null},c=function(){this.type="react-native",this.name="react-native",this.version=null,this.os=null},d=/(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/,f=3,m=[["aol",/AOLShield\/([0-9\._]+)/],["edge",/Edge\/([0-9\._]+)/],["edge-ios",/EdgiOS\/([0-9\._]+)/],["yandexbrowser",/YaBrowser\/([0-9\._]+)/],["kakaotalk",/KAKAOTALK\s([0-9\.]+)/],["samsung",/SamsungBrowser\/([0-9\.]+)/],["silk",/\bSilk\/([0-9._-]+)\b/],["miui",/MiuiBrowser\/([0-9\.]+)$/],["beaker",/BeakerBrowser\/([0-9\.]+)/],["edge-chromium",/EdgA?\/([0-9\.]+)/],["chromium-webview",/(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["chrome",/(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["phantomjs",/PhantomJS\/([0-9\.]+)(:?\s|$)/],["crios",/CriOS\/([0-9\.]+)(:?\s|$)/],["firefox",/Firefox\/([0-9\.]+)(?:\s|$)/],["fxios",/FxiOS\/([0-9\.]+)/],["opera-mini",/Opera Mini.*Version\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)(?:\s|$)/],["opera",/OPR\/([0-9\.]+)(:?\s|$)/],["pie",/^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/],["pie",/^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/],["netfront",/^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/],["ie",/Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],["ie",/MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],["ie",/MSIE\s(7\.0)/],["bb10",/BB10;\sTouch.*Version\/([0-9\.]+)/],["android",/Android\s([0-9\.]+)/],["ios",/Version\/([0-9\._]+).*Mobile.*Safari.*/],["safari",/Version\/([0-9\._]+).*Safari/],["facebook",/FB[AS]V\/([0-9\.]+)/],["instagram",/Instagram\s([0-9\.]+)/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Mobile/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Gecko\)$/],["curl",/^curl\/([0-9\.]+)$/],["searchbot",/alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/]],p=[["iOS",/iP(hone|od|ad)/],["Android OS",/Android/],["BlackBerry OS",/BlackBerry|BB10/],["Windows Mobile",/IEMobile/],["Amazon OS",/Kindle/],["Windows 3.11",/Win16/],["Windows 95",/(Windows 95)|(Win95)|(Windows_95)/],["Windows 98",/(Windows 98)|(Win98)/],["Windows 2000",/(Windows NT 5.0)|(Windows 2000)/],["Windows XP",/(Windows NT 5.1)|(Windows XP)/],["Windows Server 2003",/(Windows NT 5.2)/],["Windows Vista",/(Windows NT 6.0)/],["Windows 7",/(Windows NT 6.1)/],["Windows 8",/(Windows NT 6.2)/],["Windows 8.1",/(Windows NT 6.3)/],["Windows 10",/(Windows NT 10.0)/],["Windows ME",/Windows ME/],["Windows CE",/Windows CE|WinCE|Microsoft Pocket Internet Explorer/],["Open BSD",/OpenBSD/],["Sun OS",/SunOS/],["Chrome OS",/CrOS/],["Linux",/(Linux)|(X11)/],["Mac OS",/(Mac_PowerPC)|(Macintosh)/],["QNX",/QNX/],["BeOS",/BeOS/],["OS/2",/OS\/2/]];function w(e){var t=function(e){return""!==e&&m.reduce((function(t,n){var o=n[0],r=n[1];if(t)return t;var i=r.exec(e);return!!i&&[o,i]}),!1)}(e);if(!t)return null;var n=t[0],o=t[1];if("searchbot"===n)return new u;var r=o[1]&&o[1].split(".").join("_").split("_").slice(0,3);r?r.length<f&&(r=i(i([],r,!0),function(e){for(var t=[],n=0;n<e;n++)t.push("0");return t}(f-r.length),!0)):r=[];var s=r.join("."),c=function(e){for(var t=0,n=p.length;t<n;t++){var o=p[t],r=o[0];if(o[1].exec(e))return r}return null}(e),w=d.exec(e);return w&&w[1]?new l(n,s,c,w[1]):new a(n,s,c)}var h,g,v,y,b,S,O,k,W,E,T,I,N=h?w(h):"undefined"==typeof document&&"undefined"!=typeof navigator&&"ReactNative"===navigator.product?new c:"undefined"!=typeof navigator?w(navigator.userAgent):"undefined"!=typeof process&&process.version?new s(process.version.slice(1)):null,x={};function D(e,t){if(!v.on)return!1;var n=null;t&&(n=t(e));for(var o,r=(o=e.timeStamp&&e.timeStamp>0?v.time(e.timeStamp):Date.now(),{milli:Math.floor(o),micro:Number((o%1).toFixed(3))}),i={target:B(e.target),path:P(e),pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:r.milli,microTime:r.micro,location:A(e),scrnRes:M(),type:e.type,logType:"raw",userAction:!0,details:n,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},a=0,s=Object.values(x);a<s.length;a++){var l=s[a];if("function"==typeof l&&!(i=l(i,e)))return!1}return g.push(i),!0}function C(e,t,n){if(!v.on)return!1;var o=null;t&&(o=t());for(var r={pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:Date.now(),scrnRes:M(),logType:"custom",userAction:n,details:o,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},i=Object.assign(r,e),a=0,s=Object.values(x);a<s.length;a++){var l=s[a];if("function"==typeof l&&!(i=l(i,null)))return!1}return g.push(i),!0}function A(e){return null!=e.pageX?{x:e.pageX,y:e.pageY}:null!=e.clientX?{x:document.documentElement.scrollLeft+e.clientX,y:document.documentElement.scrollTop+e.clientY}:{x:null,y:null}}function M(){return{width:window.innerWidth,height:window.innerHeight}}function B(e){return e.localName?e.localName+(e.id?"#"+e.id:"")+(e.className?"."+e.className:""):e.nodeName?e.nodeName+(e.id?"#"+e.id:"")+(e.className?"."+e.className:""):e&&e.document&&e.location&&e.alert&&e.setInterval?"Window":"Unknown"}function P(e){if(e instanceof window.Event)return function(e){var t,n=0,o=[];for(;t=e[n];)o.push(B(t)),++n;return o}(e.composedPath())}function j(){return{browser:N?N.name:"",version:N?N.version:""}}var K,V=["click","focus","blur","input","change","mouseover","submit"],L=["load","blur","focus"];function R(e){return{clicks:e.detail,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}function X(e){return function(e){E={click:R,dblclick:R,mousedown:R,mouseup:R,focus:null,blur:null,input:e.logDetails?function(e){return{value:e.target.value}}:null,change:e.logDetails?function(e){return{value:e.target.value}}:null,dragstart:null,dragend:null,drag:null,drop:null,keydown:e.logDetails?function(e){return{key:e.keyCode,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}:null,mouseover:null},T={},I={wheel:function(e){return{x:e.deltaX,y:e.deltaY,z:e.deltaZ}},scroll:function(){return{x:window.scrollX,y:window.scrollY}},resize:function(){return{width:window.outerWidth,height:window.outerHeight}}},K={submit:null}}(e),Object.keys(E).forEach((function(e){document.addEventListener(e,(function(t){D(t,E[e])}),!0)})),V.forEach((function(e){document.addEventListener(e,(function(e){!function(e){var t=B(e.target),n=P(e),o=e.type,r=Math.floor(e.timeStamp&&e.timeStamp>0?v.time(e.timeStamp):Date.now());if(null==y&&(y=t,b=o,S=n,O=r,k=0),y!==t||b!==o){W={target:y,path:S,pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),count:k,duration:r-O,startTime:O,endTime:r,type:b,logType:"interval",targetChange:y!==t,typeChange:b!==o,userAction:!1,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID};for(var i=0,a=Object.values(x);i<a.length;i++){var s=a[i];if("function"==typeof s&&!(W=s(W,null)))return!1}g.push(W),y=t,b=o,S=n,O=r,k=0}y==t&&b==o&&(k+=1)}(e)}),!0)})),Object.keys(I).forEach((function(t){T[t]=!0,window.addEventListener(t,(function(n){T[t]&&(T[t]=!1,D(n,I[t]),setTimeout((function(){T[t]=!0}),e.resolution))}),!0)})),Object.keys(K).forEach((function(e){document.addEventListener(e,(function(t){D(t,E[e])}),!0)})),L.forEach((function(e){window.addEventListener(e,(function(e){D(e,(function(){return{window:!0}}))}),!0)})),!0}var $=null;function _(e,t){null!==$&&clearInterval($),$=function(e,t){return setInterval((function(){t.on&&e.length>=t.logCountThreshold&&(H(e.slice(0),t,0),e.splice(0))}),t.transmitInterval)}(e,t),function(e,t){window.addEventListener("pagehide",(function(){t.on&&e.length>0&&(navigator.sendBeacon(t.url,JSON.stringify(e)),e.splice(0))}))}(e,t)}function H(e,t,n){var o=new XMLHttpRequest,r=JSON.stringify(e);o.open("POST",t.url),t.authHeader&&o.setRequestHeader("Authorization",t.authHeader),o.setRequestHeader("Content-type","application/json;charset=UTF-8"),o.onreadystatechange=function(){4===o.readyState&&200!==o.status&&n>0&&H(e,t,n--)},o.send(r)}var z,F={},Y=[],J=Date.now();window.onload=function(){z=Date.now()},e.started=!1,F.on=!1,F.useraleVersion=n,r(F,function(){var e={};null===o&&(o=function(e,t){if(null===window.sessionStorage.getItem(e))return window.sessionStorage.setItem(e,JSON.stringify(t)),t;return JSON.parse(window.sessionStorage.getItem(e))}("userAleSessionId","session_"+String(Date.now())));var t,n=document.currentScript||(t=document.getElementsByTagName("script"))[t.length-1],r=n?n.getAttribute.bind(n):function(){return null};return e.autostart="false"!==r("data-autostart"),e.url=r("data-url")||"http://localhost:8000",e.transmitInterval=+r("data-interval")||5e3,e.logCountThreshold=+r("data-threshold")||5,e.userId=r("data-user")||null,e.version=r("data-version")||null,e.logDetails="true"===r("data-log-details"),e.resolution=+r("data-resolution")||500,e.toolName=r("data-tool")||null,e.userFromParams=r("data-user-from-params")||null,e.time=function(e){var t;if(e.timeStamp&&e.timeStamp>0){var n=Date.now()-e.timeStamp;if(n<0)t=function(){return e.timeStamp/1e3};else if(n>e.timeStamp){var o=performance.timing.navigationStart;t=function(e){return e+o}}else t=function(e){return e}}else t=function(){return Date.now()};return t}(document.createEvent("CustomEvent")),e.sessionID=r("data-session")||o,e.authHeader=r("data-auth")||null,e.custIndex=r("data-index")||null,e}()),g=Y,v=F,x=[],y=null,b=null,S=null,O=null,k=0,W=null,F.autostart&&function t(n){e.started||setTimeout((function(){var o=document.readyState;!n.autostart||"interactive"!==o&&"complete"!==o?t(n):(X(n),_(Y,n),e.started=n.on=!0,C({type:"load",logType:"raw",details:{pageLoadTime:z-J}},(function(){}),!1))}),100)}(F);var U=n;e.addCallbacks=function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return t.forEach((function(e){var t=Object.keys(e).reduce((function(t,n){return t[n]=Object.getOwnPropertyDescriptor(e,n),t}),{});Object.getOwnPropertySymbols(e).forEach((function(n){var o=Object.getOwnPropertyDescriptor(e,n);o.enumerable&&(t[n]=o)})),Object.defineProperties(x,t)})),x},e.buildPath=P,e.details=function(e,t){return{click:R,dblclick:R,mousedown:R,mouseup:R,focus:null,blur:null,input:e.logDetails?function(e){return{value:e.target.value}}:null,change:e.logDetails?function(e){return{value:e.target.value}}:null,dragstart:null,dragend:null,drag:null,drop:null,keydown:e.logDetails?function(e){return{key:e.keyCode,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}:null,mouseover:null,wheel:function(e){return{x:e.deltaX,y:e.deltaY,z:e.deltaZ}},scroll:function(){return{x:window.scrollX,y:window.scrollY}},resize:function(){return{width:window.outerWidth,height:window.outerHeight}},submit:null}[t]},e.getSelector=B,e.log=function(e){return null!==e&&"object"===t(e)&&(Y.push(e),!0)},e.options=function(e){return void 0!==e&&r(F,e),F},e.packageCustomLog=C,e.packageLog=D,e.removeCallbacks=function(e){e.forEach((function(e){Object.hasOwn(x,e)&&delete x[e]}))},e.start=function(){e.started&&!1!==F.autostart||(e.started=F.on=!0,F.autostart=!0)},e.stop=function(){e.started=F.on=!1,F.autostart=!1},e.version=U}));
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).userale={})}(this,(function(e){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t(e)}var n="2.4.0",o=null;function r(e,t){var n=e.autostart,o=t.autostart;Object.keys(t).forEach((function(n){if("userFromParams"===n){var o=(r=t[n],i=new RegExp("[?&]"+r+"(=([^&#]*)|&|#|$)"),(a=window.location.href.match(i))&&a[2]?decodeURIComponent(a[2].replace(/\+/g," ")):null);o&&(e.userId=o)}var r,i,a;e[n]=t[n]})),!1!==n&&!1!==o||(e.autostart=!1)}var i=function(e,t,n){if(n||2===arguments.length)for(var o,r=0,i=t.length;r<i;r++)!o&&r in t||(o||(o=Array.prototype.slice.call(t,0,r)),o[r]=t[r]);return e.concat(o||Array.prototype.slice.call(t))},a=function(e,t,n){this.name=e,this.version=t,this.os=n,this.type="browser"},s=function(e){this.version=e,this.type="node",this.name="node",this.os=process.platform},u=function(e,t,n,o){this.name=e,this.version=t,this.os=n,this.bot=o,this.type="bot-device"},l=function(){this.type="bot",this.bot=!0,this.name="bot",this.version=null,this.os=null},c=function(){this.type="react-native",this.name="react-native",this.version=null,this.os=null},d=/(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/,f=3,m=[["aol",/AOLShield\/([0-9\._]+)/],["edge",/Edge\/([0-9\._]+)/],["edge-ios",/EdgiOS\/([0-9\._]+)/],["yandexbrowser",/YaBrowser\/([0-9\._]+)/],["kakaotalk",/KAKAOTALK\s([0-9\.]+)/],["samsung",/SamsungBrowser\/([0-9\.]+)/],["silk",/\bSilk\/([0-9._-]+)\b/],["miui",/MiuiBrowser\/([0-9\.]+)$/],["beaker",/BeakerBrowser\/([0-9\.]+)/],["edge-chromium",/EdgA?\/([0-9\.]+)/],["chromium-webview",/(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["chrome",/(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["phantomjs",/PhantomJS\/([0-9\.]+)(:?\s|$)/],["crios",/CriOS\/([0-9\.]+)(:?\s|$)/],["firefox",/Firefox\/([0-9\.]+)(?:\s|$)/],["fxios",/FxiOS\/([0-9\.]+)/],["opera-mini",/Opera Mini.*Version\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)(?:\s|$)/],["opera",/OPR\/([0-9\.]+)(:?\s|$)/],["pie",/^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/],["pie",/^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/],["netfront",/^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/],["ie",/Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],["ie",/MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],["ie",/MSIE\s(7\.0)/],["bb10",/BB10;\sTouch.*Version\/([0-9\.]+)/],["android",/Android\s([0-9\.]+)/],["ios",/Version\/([0-9\._]+).*Mobile.*Safari.*/],["safari",/Version\/([0-9\._]+).*Safari/],["facebook",/FB[AS]V\/([0-9\.]+)/],["instagram",/Instagram\s([0-9\.]+)/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Mobile/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Gecko\)$/],["curl",/^curl\/([0-9\.]+)$/],["searchbot",/alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/]],p=[["iOS",/iP(hone|od|ad)/],["Android OS",/Android/],["BlackBerry OS",/BlackBerry|BB10/],["Windows Mobile",/IEMobile/],["Amazon OS",/Kindle/],["Windows 3.11",/Win16/],["Windows 95",/(Windows 95)|(Win95)|(Windows_95)/],["Windows 98",/(Windows 98)|(Win98)/],["Windows 2000",/(Windows NT 5.0)|(Windows 2000)/],["Windows XP",/(Windows NT 5.1)|(Windows XP)/],["Windows Server 2003",/(Windows NT 5.2)/],["Windows Vista",/(Windows NT 6.0)/],["Windows 7",/(Windows NT 6.1)/],["Windows 8",/(Windows NT 6.2)/],["Windows 8.1",/(Windows NT 6.3)/],["Windows 10",/(Windows NT 10.0)/],["Windows ME",/Windows ME/],["Windows CE",/Windows CE|WinCE|Microsoft Pocket Internet Explorer/],["Open BSD",/OpenBSD/],["Sun OS",/SunOS/],["Chrome OS",/CrOS/],["Linux",/(Linux)|(X11)/],["Mac OS",/(Mac_PowerPC)|(Macintosh)/],["QNX",/QNX/],["BeOS",/BeOS/],["OS/2",/OS\/2/]];function h(e){var t=function(e){return""!==e&&m.reduce((function(t,n){var o=n[0],r=n[1];if(t)return t;var i=r.exec(e);return!!i&&[o,i]}),!1)}(e);if(!t)return null;var n=t[0],o=t[1];if("searchbot"===n)return new l;var r=o[1]&&o[1].split(".").join("_").split("_").slice(0,3);r?r.length<f&&(r=i(i([],r,!0),function(e){for(var t=[],n=0;n<e;n++)t.push("0");return t}(f-r.length),!0)):r=[];var s=r.join("."),c=function(e){for(var t=0,n=p.length;t<n;t++){var o=p[t],r=o[0];if(o[1].exec(e))return r}return null}(e),h=d.exec(e);return h&&h[1]?new u(n,s,c,h[1]):new a(n,s,c)}var w,g,v,y,b,S,O,k,E,W,T,I,N=w?h(w):"undefined"==typeof document&&"undefined"!=typeof navigator&&"ReactNative"===navigator.product?new c:"undefined"!=typeof navigator?h(navigator.userAgent):"undefined"!=typeof process&&process.version?new s(process.version.slice(1)):null,x={};function D(e,t){if(!v.on)return!1;var n=null;t&&(n=t(e));for(var o,r=(o=e.timeStamp&&e.timeStamp>0?v.time(e.timeStamp):Date.now(),{milli:Math.floor(o),micro:Number((o%1).toFixed(3))}),i={target:B(e.target),path:P(e),pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:r.milli,microTime:r.micro,location:A(e),scrnRes:M(),type:e.type,logType:"raw",userAction:!0,details:n,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},a=0,s=Object.values(x);a<s.length;a++){var u=s[a];if("function"==typeof u&&!(i=u(i,e)))return!1}return g.push(i),!0}function C(e,t,n){if(!v.on)return!1;var o=null;t&&(o=t());for(var r={pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:Date.now(),scrnRes:M(),logType:"custom",userAction:n,details:o,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},i=Object.assign(r,e),a=0,s=Object.values(x);a<s.length;a++){var u=s[a];if("function"==typeof u&&!(i=u(i,null)))return!1}return g.push(i),!0}function A(e){return null!=e.pageX?{x:e.pageX,y:e.pageY}:null!=e.clientX?{x:document.documentElement.scrollLeft+e.clientX,y:document.documentElement.scrollTop+e.clientY}:{x:null,y:null}}function M(){return{width:window.innerWidth,height:window.innerHeight}}function B(e){return e.localName?e.localName+(e.id?"#"+e.id:"")+(e.className?"."+e.className:""):e.nodeName?e.nodeName+(e.id?"#"+e.id:"")+(e.className?"."+e.className:""):e&&e.document&&e.location&&e.alert&&e.setInterval?"Window":"Unknown"}function P(e){if(e instanceof window.Event)return function(e){var t,n=0,o=[];for(;t=e[n];)o.push(B(t)),++n;return o}(e.composedPath())}function j(){return{browser:N?N.name:"",version:N?N.version:""}}var K,V=["click","focus","blur","input","change","mouseover","submit"],L=["load","blur","focus"];function R(e){return{clicks:e.detail,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}function X(e){return function(e){W={click:R,dblclick:R,mousedown:R,mouseup:R,focus:null,blur:null,input:e.logDetails?function(e){return{value:e.target.value}}:null,change:e.logDetails?function(e){return{value:e.target.value}}:null,dragstart:null,dragend:null,drag:null,drop:null,keydown:e.logDetails?function(e){return{key:e.keyCode,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}:null,mouseover:null},T={},I={wheel:function(e){return{x:e.deltaX,y:e.deltaY,z:e.deltaZ}},scroll:function(){return{x:window.scrollX,y:window.scrollY}},resize:function(){return{width:window.outerWidth,height:window.outerHeight}}},K={submit:null}}(e),Object.keys(W).forEach((function(e){document.addEventListener(e,(function(t){D(t,W[e])}),!0)})),V.forEach((function(e){document.addEventListener(e,(function(e){!function(e){var t=B(e.target),n=P(e),o=e.type,r=Math.floor(e.timeStamp&&e.timeStamp>0?v.time(e.timeStamp):Date.now());if(null==y&&(y=t,b=o,S=n,O=r,k=0),y!==t||b!==o){E={target:y,path:S,pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),count:k,duration:r-O,startTime:O,endTime:r,type:b,logType:"interval",targetChange:y!==t,typeChange:b!==o,userAction:!1,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID};for(var i=0,a=Object.values(x);i<a.length;i++){var s=a[i];if("function"==typeof s&&!(E=s(E,null)))return!1}g.push(E),y=t,b=o,S=n,O=r,k=0}y==t&&b==o&&(k+=1)}(e)}),!0)})),Object.keys(I).forEach((function(t){T[t]=!0,window.addEventListener(t,(function(n){T[t]&&(T[t]=!1,D(n,I[t]),setTimeout((function(){T[t]=!0}),e.resolution))}),!0)})),Object.keys(K).forEach((function(e){document.addEventListener(e,(function(t){D(t,W[e])}),!0)})),L.forEach((function(e){window.addEventListener(e,(function(e){D(e,(function(){return{window:!0}}))}),!0)})),!0}var $=null;var _=null;function H(e,t){null!==_&&clearInterval(_),_=function(e,t){return setInterval((function(){t.on&&e.length>=t.logCountThreshold&&(z(e.slice(0),t,0),e.splice(0))}),t.transmitInterval)}(e,t),function(e,t){window.addEventListener("pagehide",(function(){t.on&&e.length>0&&(navigator.sendBeacon(t.url,JSON.stringify(e)),e.splice(0))}))}(e,t)}function z(e,t,n){var o=new XMLHttpRequest,r=JSON.stringify(e);o.open("POST",t.url),function(e){if($)try{e.authHeader=$()}catch(e){console.error("Error encountered while setting the auth header: ".concat(e))}}(t),t.authHeader&&o.setRequestHeader("Authorization",t.authHeader),o.setRequestHeader("Content-type","application/json;charset=UTF-8"),o.onreadystatechange=function(){4===o.readyState&&200!==o.status&&n>0&&z(e,t,n--)},o.send(r)}var F,U={},Y=[],J=Date.now();window.onload=function(){F=Date.now()},e.started=!1,U.on=!1,U.useraleVersion=n,r(U,function(){var e={};null===o&&(o=function(e,t){if(null===window.sessionStorage.getItem(e))return window.sessionStorage.setItem(e,JSON.stringify(t)),t;return JSON.parse(window.sessionStorage.getItem(e))}("userAleSessionId","session_"+String(Date.now())));var t,n=document.currentScript||(t=document.getElementsByTagName("script"))[t.length-1],r=n?n.getAttribute.bind(n):function(){return null};return e.autostart="false"!==r("data-autostart"),e.url=r("data-url")||"http://localhost:8000",e.transmitInterval=+r("data-interval")||5e3,e.logCountThreshold=+r("data-threshold")||5,e.userId=r("data-user")||null,e.version=r("data-version")||null,e.logDetails="true"===r("data-log-details"),e.resolution=+r("data-resolution")||500,e.toolName=r("data-tool")||null,e.userFromParams=r("data-user-from-params")||null,e.time=function(e){var t;if(e.timeStamp&&e.timeStamp>0){var n=Date.now()-e.timeStamp;if(n<0)t=function(){return e.timeStamp/1e3};else if(n>e.timeStamp){var o=performance.timing.navigationStart;t=function(e){return e+o}}else t=function(e){return e}}else t=function(){return Date.now()};return t}(document.createEvent("CustomEvent")),e.sessionID=r("data-session")||o,e.authHeader=r("data-auth")||null,e.custIndex=r("data-index")||null,e}()),g=Y,v=U,x=[],y=null,b=null,S=null,O=null,k=0,E=null,U.autostart&&function t(n){e.started||setTimeout((function(){var o=document.readyState;!n.autostart||"interactive"!==o&&"complete"!==o?t(n):(X(n),H(Y,n),e.started=n.on=!0,C({type:"load",logType:"raw",details:{pageLoadTime:F-J}},(function(){}),!1))}),100)}(U);var q=n;e.addCallbacks=function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return t.forEach((function(e){var t=Object.keys(e).reduce((function(t,n){return t[n]=Object.getOwnPropertyDescriptor(e,n),t}),{});Object.getOwnPropertySymbols(e).forEach((function(n){var o=Object.getOwnPropertyDescriptor(e,n);o.enumerable&&(t[n]=o)})),Object.defineProperties(x,t)})),x},e.buildPath=P,e.details=function(e,t){return{click:R,dblclick:R,mousedown:R,mouseup:R,focus:null,blur:null,input:e.logDetails?function(e){return{value:e.target.value}}:null,change:e.logDetails?function(e){return{value:e.target.value}}:null,dragstart:null,dragend:null,drag:null,drop:null,keydown:e.logDetails?function(e){return{key:e.keyCode,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey,meta:e.metaKey}}:null,mouseover:null,wheel:function(e){return{x:e.deltaX,y:e.deltaY,z:e.deltaZ}},scroll:function(){return{x:window.scrollX,y:window.scrollY}},resize:function(){return{width:window.outerWidth,height:window.outerHeight}},submit:null}[t]},e.getSelector=B,e.log=function(e){return null!==e&&"object"===t(e)&&(Y.push(e),!0)},e.options=function(e){return void 0!==e&&r(U,e),U},e.packageCustomLog=C,e.packageLog=D,e.registerAuthCallback=function(e){try{return function(e){if("function"!=typeof e)throw new Error("Userale auth callback must be a function");var t=e();if("string"!=typeof t)throw new Error("Userale auth callback must return a string")}(e),$=e,!0}catch(e){return!1}},e.removeCallbacks=function(e){e.forEach((function(e){Object.hasOwn(x,e)&&delete x[e]}))},e.start=function(){e.started&&!1!==U.autostart||(e.started=U.on=!0,U.autostart=!0)},e.stop=function(){e.started=U.on=!1,U.autostart=!1},e.version=q}));
diff --git a/src/auth.js b/src/auth.js
new file mode 100644
index 0000000..cb8099a
--- /dev/null
+++ b/src/auth.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+export let authCallback = null;
+
+/**
+ * Fetches the most up-to-date auth header string from the auth callback
+ * and updates the config object with the new value.
+ * @param {Object} config Configuration object to be updated.
+ * @param {Function} authCallback Callback used to fetch the newest header.
+ * @returns {void}
+ */
+export function updateAuthHeader(config) {
+  if (authCallback) {
+    try {
+        config.authHeader = authCallback();
+    } catch (e) {
+        // We should emit the error, but otherwise continue as this could be a temporary issue
+        // due to network connectivity or some logic inside the authCallback which is the user's
+        // responsibility.
+        console.error(`Error encountered while setting the auth header: ${e}`);
+    }
+  }
+}
+
+/**
+ * Registers the provided callback to be used when updating the auth header.
+ * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+ * @returns {boolean} Whether the operation succeeded.
+ */
+export function registerAuthCallback(callback) {
+  try {
+    verifyCallback(callback);
+    authCallback = callback;
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+/**
+ * Verify that the provided callback is a function which returns a string
+ * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+ * @throws {Error} If the callback is not a function or does not return a string.
+ * @returns {void}
+ */
+export function verifyCallback(callback) {
+  if (typeof callback !== "function") {
+    throw new Error("Userale auth callback must be a function");
+  }
+  const result = callback();
+  if (typeof result !== "string") {
+    throw new Error("Userale auth callback must return a string");
+  }
+}
+
+/**
+ * Resets the authCallback to null. Used for primarily for testing, but could be used
+ * to remove the callback in production.
+ * @returns {void}
+ */
+export function resetAuthCallback() {
+  authCallback = null;
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 6d09c71..f34c080 100644
--- a/src/main.js
+++ b/src/main.js
@@ -32,6 +32,7 @@
 
 export let started = false;
 export {defineCustomDetails as details} from './attachHandlers.js';
+export {registerAuthCallback as registerAuthCallback} from './auth.js';
 export {
     addCallbacks as addCallbacks,
     removeCallbacks as removeCallbacks,
diff --git a/src/sendLogs.js b/src/sendLogs.js
index c2da246..d420fa0 100644
--- a/src/sendLogs.js
+++ b/src/sendLogs.js
@@ -5,9 +5,9 @@
  * The ASF licenses this file to You 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.
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import { updateAuthHeader } from "./auth.js";
+
 let sendIntervalId = null;
 
 /**
@@ -39,7 +41,7 @@
  * @return {Number}        The newly created interval id.
  */
 export function sendOnInterval(logs, config) {
-  return setInterval(function() {
+  return setInterval(function () {
     if (!config.on) {
       return;
     }
@@ -57,8 +59,13 @@
  * @param  {Object} config Configuration object to be read from.
  */
 export function sendOnClose(logs, config) {
-  window.addEventListener('pagehide', function () {
+  window.addEventListener("pagehide", function () {
     if (config.on && logs.length > 0) {
+      // NOTE: sendBeacon does not support auth headers,
+      // so this will fail if auth is required.
+      // The alternative is to use fetch() with keepalive: true
+      // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description
+      // https://stackoverflow.com/a/73062712/9263449
       navigator.sendBeacon(config.url, JSON.stringify(logs));
       logs.splice(0); // clear log queue
     }
@@ -76,18 +83,18 @@
 // @todo expose config object to sendLogs replate url with config.url
 export function sendLogs(logs, config, retries) {
   const req = new XMLHttpRequest();
-
-  // @todo setRequestHeader for Auth
   const data = JSON.stringify(logs);
 
-  req.open('POST', config.url);
+  req.open("POST", config.url);
+
+  // Update headers
+  updateAuthHeader(config);
   if (config.authHeader) {
-    req.setRequestHeader('Authorization', config.authHeader)
+    req.setRequestHeader("Authorization", config.authHeader);
   }
+  req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
 
-  req.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
-
-  req.onreadystatechange = function() {
+  req.onreadystatechange = function () {
     if (req.readyState === 4 && req.status !== 200) {
       if (retries > 0) {
         sendLogs(logs, config, retries--);
diff --git a/test/auth_spec.js b/test/auth_spec.js
new file mode 100644
index 0000000..86029cb
--- /dev/null
+++ b/test/auth_spec.js
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 {expect} from 'chai';
+import sinon from 'sinon';
+import {authCallback, registerAuthCallback, resetAuthCallback, updateAuthHeader, verifyCallback} from '../src/auth';
+
+describe('verifyCallback', () => {
+    it('should not throw error for valid callback', () => {
+        const validCallback = sinon.stub().returns('someString');
+        expect(() => verifyCallback(validCallback)).to.not.throw();
+    });
+
+    it('should throw error for non-function callback', () => {
+        const nonFunctionCallback = 'notAFunction';
+        expect(() => verifyCallback(nonFunctionCallback)).to.throw('Userale auth callback must be a function');
+    });
+
+    it('should throw error for non-string callback return', () => {
+        const invalidReturnCallback = sinon.stub().returns(123);
+        expect(() => verifyCallback(invalidReturnCallback)).to.throw('Userale auth callback must return a string');
+    });
+
+    it('should not throw error for valid callback with empty string return', () => {
+        const validCallback = sinon.stub().returns('');
+        expect(() => verifyCallback(validCallback)).to.not.throw();
+    });
+});
+
+describe('registerAuthCallback', () => {
+    afterEach(() => {
+      resetAuthCallback();
+    });
+  
+    it('should register a valid callback', () => {
+      const validCallback = sinon.stub().returns('someString');
+      expect(registerAuthCallback(validCallback)).to.be.true;
+      expect(authCallback).to.equal(validCallback);
+    });
+  
+    it('should not register a non-function callback', () => {
+      const nonFunctionCallback = 'notAFunction';
+      expect(registerAuthCallback(nonFunctionCallback)).to.be.false;
+      expect(authCallback).to.be.null;
+    });
+  
+    it('should not register a callback with invalid return type', () => {
+      const invalidReturnCallback = sinon.stub().returns(123);
+      expect(registerAuthCallback(invalidReturnCallback)).to.be.false;
+      expect(authCallback).to.be.null;
+    });
+  
+    it('should register a callback with empty string return', () => {
+      const validCallback = sinon.stub().returns('');
+      expect(registerAuthCallback(validCallback)).to.be.true;
+      expect(authCallback).to.equal(validCallback);
+    });
+});
+
+describe('updateAuthHeader', () => {
+    let config;
+  
+    beforeEach(() => {
+      // Initialize config object before each test
+      config = { authHeader: null };
+    });
+
+    afterEach(() => {
+      resetAuthCallback();
+    });
+  
+    it('should update auth header when authCallback is provided', () => {
+      const validCallback = sinon.stub().returns('someString');
+      registerAuthCallback(validCallback);
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.equal('someString');
+    });
+  
+    it('should not update auth header when authCallback is not provided', () => {
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.be.null;
+    });
+  
+    it('should not update auth header when authCallback returns non-string', () => {
+      const invalidReturnCallback = sinon.stub().returns(123);
+      registerAuthCallback(invalidReturnCallback);
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.be.null;
+    });
+  
+    it('should update auth header with empty string return from authCallback', () => {
+      const validCallback = sinon.stub().returns('');
+      registerAuthCallback(validCallback);
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.equal('');
+    });
+  
+    it('should handle errors thrown during authCallback execution', () => {
+      const errorThrowingCallback = sinon.stub().throws(new Error('Callback execution failed'));
+      registerAuthCallback(errorThrowingCallback);
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.be.null;
+    });
+  
+    it('should not update auth header after unregistering authCallback', () => {
+      const validCallback = sinon.stub().returns('someString');
+      registerAuthCallback(validCallback);
+      updateAuthHeader(config, authCallback);
+      expect(config.authHeader).to.equal('someString');
+      
+      // Unregister authCallback
+      updateAuthHeader(config, null);
+      expect(config.authHeader).to.equal('someString');
+    });
+  });
\ No newline at end of file
diff --git a/test/sendLogs_spec.js b/test/sendLogs_spec.js
index 35686e6..6059724 100644
--- a/test/sendLogs_spec.js
+++ b/test/sendLogs_spec.js
@@ -17,7 +17,8 @@
 import {expect} from 'chai';
 import {JSDOM} from 'jsdom';
 import sinon from 'sinon';
-import {sendOnInterval, sendOnClose} from '../src/sendLogs';
+import {initSender, sendOnInterval, sendOnClose} from '../src/sendLogs';
+import {registerAuthCallback} from '../src/auth';
 import 'global-jsdom/register'
 
 describe('sendLogs', () => {
@@ -110,4 +111,42 @@
         global.window.dispatchEvent(new window.CustomEvent('pagehide'))
         sinon.assert.notCalled(sendBeaconSpy)
     });
+
+    it('sends logs with proper auth header when using registerCallback', (done) => {
+        let requests = []
+        const originalXMLHttpRequest = global.XMLHttpRequest;
+        const conf = { on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1 };
+        const logs = [];
+        const clock = sinon.useFakeTimers();
+        const xhr = sinon.useFakeXMLHttpRequest();
+        global.XMLHttpRequest = xhr;
+        xhr.onCreate = (xhr) => {
+            requests.push(xhr);
+        };
+    
+        // Mock the authCallback function
+        const authCallback = sinon.stub().returns('fakeAuthToken');
+        
+        // Register the authCallback
+        registerAuthCallback(authCallback);
+    
+        // Initialize sender with logs and config
+        initSender(logs, conf);
+    
+        // Simulate log entry
+        logs.push({ foo: 'bar' });
+    
+        // Trigger interval to send logs
+        clock.tick(conf.transmitInterval);
+
+        // Verify that the request has the proper auth header
+        expect(requests.length).to.equal(1);
+        expect(requests[0].requestHeaders.Authorization).to.equal('fakeAuthToken');
+    
+        // Restore XMLHttpRequest and clock
+        xhr.restore();
+        clock.restore();
+        global.XMLHttpRequest = originalXMLHttpRequest;
+        done()
+      });
 });