diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py
index 9efa535c93..fb5c37ebdc 100644
--- a/InvenTree/InvenTree/forms.py
+++ b/InvenTree/InvenTree/forms.py
@@ -21,6 +21,8 @@ from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
                                     PrependedText)
 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Field, Layout
+from dj_rest_auth.registration.serializers import RegisterSerializer
+from rest_framework import serializers
 
 from common.models import InvenTreeSetting
 from InvenTree.exceptions import log_error
@@ -206,6 +208,11 @@ class CustomSignupForm(SignupForm):
         return cleaned_data
 
 
+def registration_enabled():
+    """Determine whether user registration is enabled."""
+    return settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
+
+
 class RegistratonMixin:
     """Mixin to check if registration should be enabled."""
 
@@ -214,7 +221,7 @@ class RegistratonMixin:
 
         Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, default: `LOGIN_ENABLE_REG`.
         """
-        if settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')):
+        if registration_enabled():
             return super().is_open_for_signup(request, *args, **kwargs)
         return False
 
@@ -319,3 +326,20 @@ class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocial
 
         # Otherwise defer to the original allauth adapter.
         return super().login(request, user)
+
+
+# override dj-rest-auth
+class CustomRegisterSerializer(RegisterSerializer):
+    """Override of serializer to use dynamic settings."""
+    email = serializers.EmailField()
+
+    def __init__(self, instance=None, data=..., **kwargs):
+        """Check settings to influence which fields are needed."""
+        kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
+        super().__init__(instance, data, **kwargs)
+
+    def save(self, request):
+        """Override to check if registration is open."""
+        if registration_enabled():
+            return super().save(request)
+        raise forms.ValidationError(_('Registration is disabled.'))
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index fc30387d6a..68d8d45659 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -245,6 +245,8 @@ INSTALLED_APPS = [
     'django_otp.plugins.otp_static',        # Backup codes
 
     'allauth_2fa',                          # MFA flow for allauth
+    'dj_rest_auth',                         # Authentication APIs - dj-rest-auth
+    'dj_rest_auth.registration',            # Registration APIs - dj-rest-auth'
     'drf_spectacular',                      # API documentation
 
     'django_ical',                          # For exporting calendars
@@ -380,6 +382,23 @@ if DEBUG:
     # Enable browsable API if in DEBUG mode
     REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('rest_framework.renderers.BrowsableAPIRenderer')
 
+# dj-rest-auth
+# JWT switch
+USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False)
+REST_USE_JWT = USE_JWT
+OLD_PASSWORD_FIELD_ENABLED = True
+REST_AUTH_REGISTER_SERIALIZERS = {'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'}
+
+# JWT settings - rest_framework_simplejwt
+if USE_JWT:
+    JWT_AUTH_COOKIE = 'inventree-auth'
+    JWT_AUTH_REFRESH_COOKIE = 'inventree-token'
+    REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] + (
+        'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
+    )
+    INSTALLED_APPS.append('rest_framework_simplejwt')
+
+# WSGI default setting
 SPECTACULAR_SETTINGS = {
     'TITLE': 'InvenTree API',
     'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system',
diff --git a/InvenTree/InvenTree/social_auth_urls.py b/InvenTree/InvenTree/social_auth_urls.py
new file mode 100644
index 0000000000..78fed2f7c1
--- /dev/null
+++ b/InvenTree/InvenTree/social_auth_urls.py
@@ -0,0 +1,127 @@
+"""API endpoints for social authentication with allauth."""
+import logging
+from importlib import import_module
+
+from django.urls import include, path, reverse
+
+from allauth.socialaccount import providers
+from allauth.socialaccount.models import SocialApp
+from allauth.socialaccount.providers.keycloak.views import \
+    KeycloakOAuth2Adapter
+from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
+                                                          OAuth2LoginView)
+from rest_framework.generics import ListAPIView
+from rest_framework.permissions import AllowAny
+from rest_framework.response import Response
+
+from common.models import InvenTreeSetting
+
+logger = logging.getLogger('inventree')
+
+
+class GenericOAuth2ApiLoginView(OAuth2LoginView):
+    """Api view to login a user with a social account"""
+    def dispatch(self, request, *args, **kwargs):
+        """Dispatch the regular login view directly."""
+        return self.login(request, *args, **kwargs)
+
+
+class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
+    """Api view to connect a social account to the current user"""
+
+    def dispatch(self, request, *args, **kwargs):
+        """Dispatch the connect request directly."""
+
+        # Override the request method be in connection mode
+        request.GET = request.GET.copy()
+        request.GET['process'] = 'connect'
+
+        # Resume the dispatch
+        return super().dispatch(request, *args, **kwargs)
+
+
+def handle_oauth2(adapter: OAuth2Adapter):
+    """Define urls for oauth2 endpoints."""
+    return [
+        path('login/', GenericOAuth2ApiLoginView.adapter_view(adapter), name=f'{provider.id}_api_login'),
+        path('connect/', GenericOAuth2ApiConnectView.adapter_view(adapter), name=f'{provider.id}_api_connect'),
+    ]
+
+
+def handle_keycloak():
+    """Define urls for keycloak."""
+    return [
+        path('login/', GenericOAuth2ApiLoginView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_login'),
+        path('connect/', GenericOAuth2ApiConnectView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_connet'),
+    ]
+
+
+legacy = {
+    'twitter': 'twitter_oauth2',
+    'bitbucket': 'bitbucket_oauth2',
+    'linkedin': 'linkedin_oauth2',
+    'vimeo': 'vimeo_oauth2',
+    'openid': 'openid_connect',
+}  # legacy connectors
+
+
+# Collect urls for all loaded providers
+social_auth_urlpatterns = []
+
+provider_urlpatterns = []
+for provider in providers.registry.get_list():
+    try:
+        prov_mod = import_module(provider.get_package() + ".views")
+    except ImportError:
+        continue
+
+    # Try to extract the adapter class
+    adapters = [cls for cls in prov_mod.__dict__.values() if isinstance(cls, type) and not cls == OAuth2Adapter and issubclass(cls, OAuth2Adapter)]
+
+    # Get urls
+    urls = []
+    if len(adapters) == 1:
+        urls = handle_oauth2(adapter=adapters[0])
+    else:
+        if provider.id in legacy:
+            logger.warning(f'`{provider.id}` is not supported on platform UI. Use `{legacy[provider.id]}` instead.')
+            continue
+        elif provider.id == 'keycloak':
+            urls = handle_keycloak()
+        else:
+            logger.error(f'Found handler that is not yet ready for platform UI: `{provider.id}`. Open an feature request on GitHub if you need it implemented.')
+            continue
+    provider_urlpatterns += [path(f'{provider.id}/', include(urls))]
+
+
+social_auth_urlpatterns += provider_urlpatterns
+
+
+class SocialProvierListView(ListAPIView):
+    """List of available social providers."""
+    permission_classes = (AllowAny,)
+
+    def get(self, request, *args, **kwargs):
+        """Get the list of providers."""
+        provider_list = []
+        for provider in providers.registry.get_list():
+            provider_data = {
+                'id': provider.id,
+                'name': provider.name,
+                'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')),
+                'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')),
+            }
+            try:
+                provider_data['display_name'] = provider.get_app(request).name
+            except SocialApp.DoesNotExist:
+                provider_data['display_name'] = provider.name
+
+            provider_list.append(provider_data)
+
+        data = {
+            'sso_enabled': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'),
+            'sso_registration': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'),
+            'mfa_required': InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA'),
+            'providers': provider_list
+        }
+        return Response(data)
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index e98268db9c..0cf125f32a 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -9,6 +9,8 @@ from django.contrib import admin
 from django.urls import include, path, re_path
 from django.views.generic.base import RedirectView
 
+from dj_rest_auth.registration.views import (SocialAccountDisconnectView,
+                                             SocialAccountListView)
 from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
 
 from build.api import build_api_urls
@@ -31,6 +33,7 @@ from stock.urls import stock_urls
 from users.api import user_urls
 
 from .api import APISearchView, InfoView, NotFoundView
+from .social_auth_urls import SocialProvierListView, social_auth_urlpatterns
 from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
                     CustomEmailView, CustomLoginView,
                     CustomPasswordResetFromKeyView,
@@ -71,6 +74,14 @@ apipatterns = [
     # InvenTree information endpoint
     path('', InfoView.as_view(), name='api-inventree-info'),
 
+    # Third party API endpoints
+    path('auth/', include('dj_rest_auth.urls')),
+    path('auth/registration/', include('dj_rest_auth.registration.urls')),
+    path('auth/providers/', SocialProvierListView.as_view(), name='social_providers'),
+    path('auth/social/', include(social_auth_urlpatterns)),
+    path('auth/social/', SocialAccountListView.as_view(), name='social_account_list'),
+    path('auth/social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
+
     # Unknown endpoint
     re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
 ]
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 7795bb694f..2acbbfbba1 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -183,6 +183,11 @@ login_default_protocol: http
 remote_login_enabled: False
 remote_login_header: HTTP_REMOTE_USER
 
+# JWT tokens
+# JWT can be used optionally to authenticate users. Turned off by default.
+# Alternatively, use the environment variable INVENTREE_USE_JWT
+# use_jwt: True
+
 # Logout redirect configuration
 # This setting may be required if using remote / proxy login to redirect requests
 # during the logout process (default is 'index'). Please read the docs for more details
diff --git a/requirements.in b/requirements.in
index a1a6525479..be13dc3807 100644
--- a/requirements.in
+++ b/requirements.in
@@ -28,7 +28,9 @@ django-taggit                           # Tagging support
 django-user-sessions                    # user sessions in DB
 django-weasyprint                       # django weasyprint integration
 djangorestframework                     # DRF framework
+djangorestframework-simplejwt[crypto]   # JWT authentication
 django-xforwardedfor-middleware         # IP forwarding metadata
+dj-rest-auth                            # Authentication API endpoints
 dulwich                                 # pure Python git integration
 drf-spectacular                         # DRF API documentation
 feedparser                              # RSS newsfeed parser
diff --git a/requirements.txt b/requirements.txt
index c1d35125d5..71a9ea8a32 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -35,6 +35,7 @@ coreschema==0.0.4
 cryptography==41.0.1
     # via
     #   -r requirements.in
+    #   djangorestframework-simplejwt
     #   pyjwt
 cssselect2==0.7.0
     # via weasyprint
@@ -44,9 +45,12 @@ defusedxml==0.7.1
     #   python3-openid
 diff-match-patch==20230430
     # via django-import-export
+dj-rest-auth==4.0.1
+    # via -r requirements.in
 django==3.2.19
     # via
     #   -r requirements.in
+    #   dj-rest-auth
     #   django-allauth
     #   django-allauth-2fa
     #   django-cors-headers
@@ -74,6 +78,7 @@ django==3.2.19
     #   django-weasyprint
     #   django-xforwardedfor-middleware
     #   djangorestframework
+    #   djangorestframework-simplejwt
     #   drf-spectacular
 django-allauth==0.54.0
     # via
@@ -140,7 +145,11 @@ django-xforwardedfor-middleware==2.0
 djangorestframework==3.14.0
     # via
     #   -r requirements.in
+    #   dj-rest-auth
+    #   djangorestframework-simplejwt
     #   drf-spectacular
+djangorestframework-simplejwt[crypto]==5.2.2
+    # via -r requirements.in
 drf-spectacular==0.26.2
     # via -r requirements.in
 dulwich==0.21.5
@@ -202,7 +211,9 @@ pycparser==2.21
 pydyf==0.6.0
     # via weasyprint
 pyjwt[crypto]==2.7.0
-    # via django-allauth
+    # via
+    #   django-allauth
+    #   djangorestframework-simplejwt
 pyphen==0.14.0
     # via weasyprint
 pypng==0.20220715.0