diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs
index cffdbeab9b..aef1517bb5 100644
--- a/bitwarden_license/src/Sso/Controllers/AccountController.cs
+++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs
@@ -8,10 +8,12 @@ using Bit.Core.Entities;
 using Bit.Core.Enums;
 using Bit.Core.Models;
 using Bit.Core.Models.Api;
+using Bit.Core.Models.Business.Tokenables;
 using Bit.Core.Models.Data;
 using Bit.Core.Repositories;
 using Bit.Core.Services;
 using Bit.Core.Settings;
+using Bit.Core.Tokens;
 using Bit.Core.Utilities;
 using Bit.Sso.Models;
 using Bit.Sso.Utilities;
@@ -47,6 +49,7 @@ namespace Bit.Sso.Controllers
         private readonly UserManager<User> _userManager;
         private readonly IGlobalSettings _globalSettings;
         private readonly Core.Services.IEventService _eventService;
+        private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
 
         public AccountController(
             IAuthenticationSchemeProvider schemeProvider,
@@ -64,7 +67,8 @@ namespace Bit.Sso.Controllers
             II18nService i18nService,
             UserManager<User> userManager,
             IGlobalSettings globalSettings,
-            Core.Services.IEventService eventService)
+            Core.Services.IEventService eventService,
+            IDataProtectorTokenFactory<SsoTokenable> dataProtector)
         {
             _schemeProvider = schemeProvider;
             _clientStore = clientStore;
@@ -82,57 +86,47 @@ namespace Bit.Sso.Controllers
             _userManager = userManager;
             _eventService = eventService;
             _globalSettings = globalSettings;
+            _dataProtector = dataProtector;
         }
 
         [HttpGet]
         public async Task<IActionResult> PreValidate(string domainHint)
         {
-            IActionResult invalidJson(string errorMessageKey, Exception ex = null)
-            {
-                Response.StatusCode = ex == null ? 400 : 500;
-                return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
-                {
-                    ExceptionMessage = ex?.Message,
-                    ExceptionStackTrace = ex?.StackTrace,
-                    InnerExceptionMessage = ex?.InnerException?.Message,
-                });
-            }
-
             try
             {
                 // Validate domain_hint provided
                 if (string.IsNullOrWhiteSpace(domainHint))
                 {
-                    return invalidJson("NoOrganizationIdentifierProvidedError");
+                    return InvalidJson("NoOrganizationIdentifierProvidedError");
                 }
 
                 // Validate organization exists from domain_hint
                 var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
                 if (organization == null)
                 {
-                    return invalidJson("OrganizationNotFoundByIdentifierError");
+                    return InvalidJson("OrganizationNotFoundByIdentifierError");
                 }
                 if (!organization.UseSso)
                 {
-                    return invalidJson("SsoNotAllowedForOrganizationError");
+                    return InvalidJson("SsoNotAllowedForOrganizationError");
                 }
 
                 // Validate SsoConfig exists and is Enabled
                 var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
                 if (ssoConfig == null)
                 {
-                    return invalidJson("SsoConfigurationNotFoundForOrganizationError");
+                    return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
                 }
                 if (!ssoConfig.Enabled)
                 {
-                    return invalidJson("SsoNotEnabledForOrganizationError");
+                    return InvalidJson("SsoNotEnabledForOrganizationError");
                 }
 
                 // Validate Authentication Scheme exists and is loaded (cache)
                 var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
                 if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
                 {
-                    return invalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
+                    return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
                 }
 
                 // Run scheme validation
@@ -148,37 +142,60 @@ namespace Bit.Sso.Controllers
                     {
                         errorKey = ex.Message;
                     }
-                    return invalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
+                    return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
                 }
+
+                var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
+                var token = _dataProtector.Protect(tokenable);
+
+                return new SsoPreValidateResponseModel(token);
             }
             catch (Exception ex)
             {
-                return invalidJson("PreValidationError", ex);
+                return InvalidJson("PreValidationError", ex);
             }
-
-            // Everything is good!
-            return new EmptyResult();
         }
 
         [HttpGet]
         public async Task<IActionResult> Login(string returnUrl)
         {
             var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
-            if (context.Parameters.AllKeys.Contains("domain_hint") &&
-                !string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
-            {
-                return RedirectToAction(nameof(ExternalChallenge), new
-                {
-                    scheme = context.Parameters["domain_hint"],
-                    returnUrl,
-                    state = context.Parameters["state"],
-                    userIdentifier = context.Parameters["session_state"]
-                });
-            }
-            else
+
+            if (!context.Parameters.AllKeys.Contains("domain_hint") ||
+                string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
             {
                 throw new Exception(_i18nService.T("NoDomainHintProvided"));
             }
+
+            var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];
+
+            if (string.IsNullOrWhiteSpace(ssoToken))
+            {
+                return Unauthorized("A valid SSO token is required to continue with SSO login");
+            }
+
+            var domainHint = context.Parameters["domain_hint"];
+            var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
+
+            if (organization == null)
+            {
+                return InvalidJson("OrganizationNotFoundByIdentifierError");
+            }
+
+            var tokenable = _dataProtector.Unprotect(ssoToken);
+
+            if (!tokenable.TokenIsValid(organization))
+            {
+                return Unauthorized("The SSO token associated with your request is expired. A valid SSO token is required to continue.");
+            }
+
+            return RedirectToAction(nameof(ExternalChallenge), new
+            {
+                scheme = organization.Id.ToString(),
+                returnUrl,
+                state = context.Parameters["state"],
+                userIdentifier = context.Parameters["session_state"],
+            });
         }
 
         [HttpGet]
@@ -548,6 +565,17 @@ namespace Bit.Sso.Controllers
             return user;
         }
 
+        private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
+        {
+            Response.StatusCode = ex == null ? 400 : 500;
+            return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
+            {
+                ExceptionMessage = ex?.Message,
+                ExceptionStackTrace = ex?.StackTrace,
+                InnerExceptionMessage = ex?.InnerException?.Message,
+            });
+        }
+
         private string GetEmailAddress(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
         {
             var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
diff --git a/bitwarden_license/src/Sso/Models/SsoPreValidateResponseModel.cs b/bitwarden_license/src/Sso/Models/SsoPreValidateResponseModel.cs
new file mode 100644
index 0000000000..9877e1c5ac
--- /dev/null
+++ b/bitwarden_license/src/Sso/Models/SsoPreValidateResponseModel.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Bit.Sso.Models
+{
+    public class SsoPreValidateResponseModel : JsonResult
+    {
+        public SsoPreValidateResponseModel(string token) : base(new
+        {
+            token
+        })
+        { }
+    }
+}
diff --git a/src/Core/Models/Business/Tokenables/SsoTokenable.cs b/src/Core/Models/Business/Tokenables/SsoTokenable.cs
new file mode 100644
index 0000000000..3eb9b1ae66
--- /dev/null
+++ b/src/Core/Models/Business/Tokenables/SsoTokenable.cs
@@ -0,0 +1,46 @@
+
+using System;
+using System.Text.Json.Serialization;
+using Bit.Core.Entities;
+using Bit.Core.Tokens;
+
+namespace Bit.Core.Models.Business.Tokenables
+{
+    public class SsoTokenable : ExpiringTokenable
+    {
+        public const string ClearTextPrefix = "BWUserPrefix_";
+        public const string DataProtectorPurpose = "SsoTokenDataProtector";
+        public const string TokenIdentifier = "ssoToken";
+
+        public Guid OrganizationId { get; set; }
+        public string DomainHint { get; set; }
+        public string Identifier { get; set; } = TokenIdentifier;
+
+        [JsonConstructor]
+        public SsoTokenable() { }
+
+        public SsoTokenable(Organization organization, double tokenLifetimeInSeconds) : this()
+        {
+            OrganizationId = organization?.Id ?? default;
+            DomainHint = organization?.Identifier;
+            ExpirationDate = DateTime.UtcNow.AddSeconds(tokenLifetimeInSeconds);
+        }
+
+        public bool TokenIsValid(Organization organization)
+        {
+            if (OrganizationId == default || DomainHint == default || organization == null || !Valid)
+            {
+                return false;
+            }
+
+            return organization.Identifier.Equals(DomainHint, StringComparison.InvariantCultureIgnoreCase)
+                && organization.Id.Equals(OrganizationId);
+        }
+
+        // Validates deserialized 
+        protected override bool TokenIsValid() =>
+            Identifier == TokenIdentifier
+            && OrganizationId != default
+            && !string.IsNullOrWhiteSpace(DomainHint);
+    }
+}
diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx
index af0c6a1098..eacc29d68b 100644
--- a/src/Core/Resources/SharedResources.en.resx
+++ b/src/Core/Resources/SharedResources.en.resx
@@ -679,4 +679,10 @@
   <data name="IdpSingleSignOnServiceUrlInvalid" xml:space="preserve">
     <value>Single sign on service URL contains illegal characters.</value>
   </data>
+  <data name="SsoRedirectTokenValidationMissing" xml:space="preserve">
+    <value>Single sign on redirect token is missing from the request.</value>
+  </data>
+  <data name="InvalidSsoRedirectToken" xml:space="preserve">
+    <value>Single sign on redirect token is invalid or expired.</value>
+  </data>
 </root>
\ No newline at end of file
diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs
index 8cf3a99c2e..5b1a0e6c77 100644
--- a/src/Core/Settings/GlobalSettings.cs
+++ b/src/Core/Settings/GlobalSettings.cs
@@ -67,7 +67,7 @@ namespace Bit.Core.Settings
         public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();
         public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();
         public virtual AppleIapSettings AppleIap { get; set; } = new AppleIapSettings();
-        public virtual SsoSettings Sso { get; set; } = new SsoSettings();
+        public virtual ISsoSettings Sso { get; set; } = new SsoSettings();
         public virtual StripeSettings Stripe { get; set; } = new StripeSettings();
         public virtual ITwoFactorAuthSettings TwoFactorAuth { get; set; } = new TwoFactorAuthSettings();
 
@@ -461,9 +461,10 @@ namespace Bit.Core.Settings
             public bool AppInReview { get; set; }
         }
 
-        public class SsoSettings
+        public class SsoSettings : ISsoSettings
         {
             public int CacheLifetimeInSeconds { get; set; } = 60;
+            public double SsoTokenLifetimeInSeconds { get; set; } = 5;
         }
 
         public class CaptchaSettings
diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs
index 8ccafdc1b3..ec648384e1 100644
--- a/src/Core/Settings/IGlobalSettings.cs
+++ b/src/Core/Settings/IGlobalSettings.cs
@@ -14,5 +14,6 @@
         IConnectionStringSettings Storage { get; set; }
         IBaseServiceUriSettings BaseServiceUri { get; set; }
         ITwoFactorAuthSettings TwoFactorAuth { get; set; }
+        ISsoSettings Sso { get; set; }
     }
 }
diff --git a/src/Core/Settings/ISsoSettings.cs b/src/Core/Settings/ISsoSettings.cs
new file mode 100644
index 0000000000..de5193cef4
--- /dev/null
+++ b/src/Core/Settings/ISsoSettings.cs
@@ -0,0 +1,8 @@
+namespace Bit.Core.Settings
+{
+    public interface ISsoSettings
+    {
+        int CacheLifetimeInSeconds { get; set; }
+        double SsoTokenLifetimeInSeconds { get; set; }
+    }
+}
diff --git a/src/Icons/packages.lock.json b/src/Icons/packages.lock.json
index f7d7c85c6e..06af2f1afc 100644
--- a/src/Icons/packages.lock.json
+++ b/src/Icons/packages.lock.json
@@ -3368,4 +3368,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/Identity/Controllers/SsoController.cs b/src/Identity/Controllers/SsoController.cs
index bb82513373..a7d0670e16 100644
--- a/src/Identity/Controllers/SsoController.cs
+++ b/src/Identity/Controllers/SsoController.cs
@@ -6,6 +6,7 @@ using System.Security.Claims;
 using System.Threading.Tasks;
 using Bit.Core.Entities;
 using Bit.Core.Models.Api;
+using Bit.Core.Models.Business.Tokenables;
 using Bit.Core.Repositories;
 using Bit.Identity.Models;
 using IdentityModel;
@@ -59,14 +60,11 @@ namespace Bit.Identity.Controllers
                 var culture = requestCultureFeature.RequestCulture.Culture.Name;
                 var requestPath = $"/Account/PreValidate?domainHint={domainHint}&culture={culture}";
                 var httpClient = _clientFactory.CreateClient("InternalSso");
+
+                // Forward the internal SSO result
                 using var responseMessage = await httpClient.GetAsync(requestPath);
-                if (responseMessage.IsSuccessStatusCode)
-                {
-                    // All is good!
-                    return new EmptyResult();
-                }
-                Response.StatusCode = (int)responseMessage.StatusCode;
                 var responseJson = await responseMessage.Content.ReadAsStringAsync();
+                Response.StatusCode = (int)responseMessage.StatusCode;
                 return Content(responseJson, "application/json");
             }
             catch (Exception ex)
@@ -89,6 +87,7 @@ namespace Bit.Identity.Controllers
 
             var domainHint = context.Parameters.AllKeys.Contains("domain_hint") ?
                 context.Parameters["domain_hint"] : null;
+            var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];
 
             if (string.IsNullOrWhiteSpace(domainHint))
             {
@@ -100,27 +99,28 @@ namespace Bit.Identity.Controllers
 
             return RedirectToAction(nameof(ExternalChallenge), new
             {
-                organizationIdentifier = domainHint,
+                domainHint = domainHint,
                 returnUrl,
-                userIdentifier
+                userIdentifier,
+                ssoToken,
             });
         }
 
         [HttpGet]
-        public async Task<IActionResult> ExternalChallenge(string organizationIdentifier, string returnUrl,
-            string userIdentifier)
+        public async Task<IActionResult> ExternalChallenge(string domainHint, string returnUrl,
+            string userIdentifier, string ssoToken)
         {
-            if (string.IsNullOrWhiteSpace(organizationIdentifier))
+            if (string.IsNullOrWhiteSpace(domainHint))
             {
                 throw new Exception("Invalid organization reference id.");
             }
 
-            var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(organizationIdentifier);
+            var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
             if (ssoConfig == null || !ssoConfig.Enabled)
             {
                 throw new Exception("Organization not found or SSO configuration not enabled");
             }
-            var domainHint = ssoConfig.OrganizationId.ToString();
+            var organizationId = ssoConfig.OrganizationId.ToString();
 
             var scheme = "sso";
             var props = new AuthenticationProperties
@@ -130,8 +130,13 @@ namespace Bit.Identity.Controllers
                 {
                     { "return_url", returnUrl },
                     { "domain_hint", domainHint },
+                    { "organizationId", organizationId },
                     { "scheme", scheme },
                 },
+                Parameters =
+                {
+                    { "ssoToken", ssoToken },
+                }
             };
 
             if (!string.IsNullOrWhiteSpace(userIdentifier))
@@ -173,7 +178,7 @@ namespace Bit.Identity.Controllers
                 IsPersistent = true,
                 ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
             };
-            if (result.Properties != null && result.Properties.Items.TryGetValue("domain_hint", out var organization))
+            if (result.Properties != null && result.Properties.Items.TryGetValue("organizationId", out var organization))
             {
                 additionalLocalClaims.Add(new Claim("organizationId", organization));
             }
diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs
index 2b3d4afff8..f1c76adaa0 100644
--- a/src/Identity/Startup.cs
+++ b/src/Identity/Startup.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using AspNetCoreRateLimit;
 using Bit.Core;
 using Bit.Core.Context;
+using Bit.Core.Models.Business.Tokenables;
 using Bit.Core.Settings;
 using Bit.Core.Utilities;
 using Bit.Identity.Utilities;
@@ -110,10 +111,17 @@ namespace Bit.Identity
                         {
                             // Pass domain_hint onto the sso idp
                             context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"];
+                            context.ProtocolMessage.Parameters.Add("organizationId", context.Properties.Items["organizationId"]);
                             if (context.Properties.Items.ContainsKey("user_identifier"))
                             {
                                 context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"];
                             }
+
+                            if (context.Properties.Parameters.Count > 0 && context.Properties.Parameters.ContainsKey(SsoTokenable.TokenIdentifier))
+                            {
+                                var token = context.Properties.Parameters[SsoTokenable.TokenIdentifier].ToString();
+                                context.ProtocolMessage.Parameters.Add(SsoTokenable.TokenIdentifier, token);
+                            }
                             return Task.FromResult(0);
                         }
                     };
diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
index c57011d0ea..b78587b8de 100644
--- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
+++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
@@ -121,6 +121,11 @@ namespace Bit.SharedWeb.Utilities
                     HCaptchaTokenable.DataProtectorPurpose,
                     serviceProvider.GetDataProtectionProvider())
             );
+            services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
+                new DataProtectorTokenFactory<SsoTokenable>(
+                    SsoTokenable.ClearTextPrefix,
+                    SsoTokenable.DataProtectorPurpose,
+                    serviceProvider.GetDataProtectionProvider()));
         }
 
         public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
diff --git a/src/SharedWeb/packages.lock.json b/src/SharedWeb/packages.lock.json
index 21a15ee405..9e1ddef459 100644
--- a/src/SharedWeb/packages.lock.json
+++ b/src/SharedWeb/packages.lock.json
@@ -3351,4 +3351,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql
index ae8780b9c1..27c286889f 100644
--- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql	
+++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql	
@@ -11,6 +11,6 @@ BEGIN
     FROM
         [dbo].[OrganizationSponsorship] OS
     WHERE
-        [SponsoringOrganizationUserId] = @OrganizationUserId
+        [SponsoringOrganizationUserID] = @OrganizationUserId
 END
 GO
diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql
index 203d40559d..3f2478f02c 100644
--- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql	
+++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql	
@@ -11,6 +11,6 @@ BEGIN
     FROM
         [dbo].[OrganizationSponsorship] OS
     INNER JOIN
-        @SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserId
+        @SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserID
 END
 GO
diff --git a/test/Core.Test/Models/Business/Tokenables/SsoTokenableTests.cs b/test/Core.Test/Models/Business/Tokenables/SsoTokenableTests.cs
new file mode 100644
index 0000000000..a7dee919bd
--- /dev/null
+++ b/test/Core.Test/Models/Business/Tokenables/SsoTokenableTests.cs
@@ -0,0 +1,90 @@
+using System;
+using AutoFixture.Xunit2;
+using Bit.Core.Entities;
+using Bit.Core.Models.Business.Tokenables;
+using Bit.Core.Tokens;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Xunit;
+
+namespace Bit.Core.Test.Models.Business.Tokenables
+{
+    public class SsoTokenableTests
+    {
+        [Fact]
+        public void CanHandleNullOrganization()
+        {
+            var token = new SsoTokenable(null, default);
+
+            Assert.Equal(default, token.OrganizationId);
+            Assert.Equal(default, token.DomainHint);
+        }
+
+        [Fact]
+        public void TokenWithNullOrganizationIsInvalid()
+        {
+            var token = new SsoTokenable(null, 500)
+            {
+                ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
+            };
+
+            Assert.False(token.Valid);
+        }
+
+        [Theory, BitAutoData]
+        public void TokenValidityCheckNullOrganizationIsInvalid(Organization organization)
+        {
+            var token = new SsoTokenable(organization, 500)
+            {
+                ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
+            };
+
+            Assert.False(token.TokenIsValid(null));
+        }
+
+        [Theory, AutoData]
+        public void SetsDataFromOrganization(Organization organization)
+        {
+            var token = new SsoTokenable(organization, default);
+
+            Assert.Equal(organization.Id, token.OrganizationId);
+            Assert.Equal(organization.Identifier, token.DomainHint);
+        }
+
+        [Fact]
+        public void SetsExpirationFromConstructor()
+        {
+            var expectedDateTime = DateTime.UtcNow.AddSeconds(500);
+            var token = new SsoTokenable(null, 500);
+
+            Assert.Equal(expectedDateTime, token.ExpirationDate, TimeSpan.FromMilliseconds(10));
+        }
+
+        [Theory, AutoData]
+        public void SerializationSetsCorrectDateTime(Organization organization)
+        {
+            var expectedDateTime = DateTime.UtcNow.AddHours(-5);
+            var token = new SsoTokenable(organization, default)
+            {
+                ExpirationDate = expectedDateTime
+            };
+
+            var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
+
+            Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
+        }
+
+        [Theory, AutoData]
+        public void TokenIsValidFailsWhenExpired(Organization organization)
+        {
+            var expectedDateTime = DateTime.UtcNow.AddHours(-5);
+            var token = new SsoTokenable(organization, default)
+            {
+                ExpirationDate = expectedDateTime
+            };
+
+            var result = token.TokenIsValid(organization);
+
+            Assert.False(result);
+        }
+    }
+}
diff --git a/util/Setup/packages.lock.json b/util/Setup/packages.lock.json
index 3b6fef04f7..e14db029b7 100644
--- a/util/Setup/packages.lock.json
+++ b/util/Setup/packages.lock.json
@@ -3289,4 +3289,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}