forked from enviPath/enviPy
[Feature] Initial Active Directory / Entra Login (#101)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#101
This commit is contained in:
@ -47,6 +47,7 @@ INSTALLED_APPS = [
|
|||||||
# Custom
|
# Custom
|
||||||
'epdb',
|
'epdb',
|
||||||
'migration',
|
'migration',
|
||||||
|
'epauth',
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
@ -351,5 +352,16 @@ LOGIN_EXEMPT_URLS = [
|
|||||||
'/o/token/',
|
'/o/token/',
|
||||||
'/o/userinfo/',
|
'/o/userinfo/',
|
||||||
'/password_reset/',
|
'/password_reset/',
|
||||||
'/reset/'
|
'/reset/',
|
||||||
|
'/microsoft/',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# MS AD/Entra
|
||||||
|
MS_ENTRA_ENABLED = os.environ.get('MS_ENTRA_ENABLED', 'False') == 'True'
|
||||||
|
if MS_ENTRA_ENABLED:
|
||||||
|
MS_ENTRA_CLIENT_ID = os.environ['MS_CLIENT_ID']
|
||||||
|
MS_ENTRA_CLIENT_SECRET = os.environ['MS_CLIENT_SECRET']
|
||||||
|
MS_ENTRA_TENANT_ID = os.environ['MS_TENANT_ID']
|
||||||
|
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
|
||||||
|
MS_ENTRA_REDIRECT_URI = os.environ['MS_REDIRECT_URI']
|
||||||
|
MS_ENTRA_SCOPES = os.environ.get('MS_SCOPES', '').split(',')
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from django.urls import include, path
|
|||||||
from .api import api_v1, api_legacy
|
from .api import api_v1, api_legacy
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("", include("epauth.urls")),
|
||||||
path("", include("epdb.urls")),
|
path("", include("epdb.urls")),
|
||||||
path("", include("migration.urls")),
|
path("", include("migration.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
|||||||
0
epauth/__init__.py
Normal file
0
epauth/__init__.py
Normal file
3
epauth/admin.py
Normal file
3
epauth/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
epauth/apps.py
Normal file
6
epauth/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class EpauthConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'epauth'
|
||||||
0
epauth/migrations/__init__.py
Normal file
0
epauth/migrations/__init__.py
Normal file
3
epauth/models.py
Normal file
3
epauth/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
epauth/tests.py
Normal file
3
epauth/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
epauth/urls.py
Normal file
8
epauth/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("microsoft/login/", views.microsoft_login, name="microsoft_login"),
|
||||||
|
path("microsoft/callback/", views.microsoft_callback, name="microsoft_callback"),
|
||||||
|
]
|
||||||
66
epauth/views.py
Normal file
66
epauth/views.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import msal
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from epdb.logic import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
def microsoft_login(request):
|
||||||
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
|
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
||||||
|
authority=s.MS_ENTRA_AUTHORITY
|
||||||
|
)
|
||||||
|
|
||||||
|
flow = msal_app.initiate_auth_code_flow(
|
||||||
|
scopes=s.MS_ENTRA_SCOPES,
|
||||||
|
redirect_uri=s.MS_ENTRA_REDIRECT_URI
|
||||||
|
)
|
||||||
|
|
||||||
|
request.session["msal_auth_flow"] = flow
|
||||||
|
return redirect(flow["auth_uri"])
|
||||||
|
|
||||||
|
|
||||||
|
def microsoft_callback(request):
|
||||||
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
client_id=s.MS_ENTRA_CLIENT_ID,
|
||||||
|
client_credential=s.MS_ENTRA_CLIENT_SECRET,
|
||||||
|
authority=s.MS_ENTRA_AUTHORITY
|
||||||
|
)
|
||||||
|
|
||||||
|
flow = request.session.pop("msal_auth_flow", None)
|
||||||
|
if not flow:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# Acquire token using the flow and callback request
|
||||||
|
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
|
||||||
|
|
||||||
|
if "access_token" in result:
|
||||||
|
# Optional: Fetch user info from Microsoft Graph
|
||||||
|
import requests
|
||||||
|
resp = requests.get(
|
||||||
|
"https://graph.microsoft.com/v1.0/me",
|
||||||
|
headers={"Authorization": f"Bearer {result['access_token']}"}
|
||||||
|
)
|
||||||
|
user_info = resp.json()
|
||||||
|
|
||||||
|
user_name = user_info["displayName"]
|
||||||
|
user_email = user_info["mail"]
|
||||||
|
user_oid = user_info["id"]
|
||||||
|
|
||||||
|
# Get implementing class
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
if User.objects.filter(uuid=user_oid).exists():
|
||||||
|
login(request, User.objects.get(uuid=user_oid))
|
||||||
|
else:
|
||||||
|
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
|
||||||
|
login(request, u)
|
||||||
|
|
||||||
|
# TODO Group Sync
|
||||||
|
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
return redirect("/") # Handle errors
|
||||||
@ -153,11 +153,17 @@ class UserManager(object):
|
|||||||
# avoid circular import :S
|
# avoid circular import :S
|
||||||
from .tasks import send_registration_mail
|
from .tasks import send_registration_mail
|
||||||
|
|
||||||
is_active = not s.ADMIN_APPROVAL_REQUIRED
|
extra_fields = {
|
||||||
if 'is_active' in kwargs:
|
'is_active': not s.ADMIN_APPROVAL_REQUIRED
|
||||||
is_active = kwargs['is_active']
|
}
|
||||||
|
|
||||||
u = get_user_model().objects.create_user(username, email, password, is_active=is_active)
|
if 'is_active' in kwargs:
|
||||||
|
extra_fields['is_active'] = kwargs['is_active']
|
||||||
|
|
||||||
|
if 'uuid' in kwargs:
|
||||||
|
extra_fields['uuid'] = kwargs['uuid']
|
||||||
|
|
||||||
|
u = get_user_model().objects.create_user(username, email, password, **kwargs)
|
||||||
|
|
||||||
# Create package
|
# Create package
|
||||||
package_name = f"{u.username}{'’' if u.username[-1] in 'sxzß' else 's'} Package"
|
package_name = f"{u.username}{'’' if u.username[-1] in 'sxzß' else 's'} Package"
|
||||||
|
|||||||
@ -31,3 +31,6 @@ dependencies = [
|
|||||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
|
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
|
||||||
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
|
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
|
||||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}
|
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
ms-login = ["msal>=1.33.0"]
|
||||||
30
uv.lock
generated
30
uv.lock
generated
@ -527,6 +527,7 @@ dependencies = [
|
|||||||
{ name = "envipy-plugins" },
|
{ name = "envipy-plugins" },
|
||||||
{ name = "epam-indigo" },
|
{ name = "epam-indigo" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
|
{ name = "msal" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "rdkit" },
|
{ name = "rdkit" },
|
||||||
@ -551,6 +552,7 @@ requires-dist = [
|
|||||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
|
{ name = "msal", specifier = ">=1.33.0" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||||
{ name = "rdkit", specifier = ">=2025.3.2" },
|
{ name = "rdkit", specifier = ">=2025.3.2" },
|
||||||
@ -854,6 +856,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
|
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "msal"
|
||||||
|
version = "1.33.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.4.4"
|
version = "6.4.4"
|
||||||
@ -1538,6 +1554,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
|
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
crypto = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
|
|||||||
Reference in New Issue
Block a user