Skip to content

Commit 92114a1

Browse files
Ajay KumarAjay Kumar
authored andcommitted
Update code for latest workos version
1 parent 0cd9585 commit 92114a1

3 files changed

Lines changed: 122 additions & 33 deletions

File tree

python-django-sso-example/sso/tests.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_auth_saml_login(self):
5555
mock_client = MagicMock()
5656
mock_client.sso = mock_sso
5757

58-
with patch.object(views.workos, "client", mock_client):
58+
with patch.object(views, "workos_client", mock_client):
5959
response = self.client.post(
6060
reverse("auth"),
6161
{"login_method": "saml"},
@@ -68,7 +68,7 @@ def test_auth_saml_login(self):
6868
self.assertIn("redirect_uri", call_args.kwargs)
6969
self.assertIn("state", call_args.kwargs)
7070
self.assertIn("organization_id", call_args.kwargs)
71-
self.assertEqual(call_args.kwargs["organization_id"], "xxx")
71+
self.assertEqual(call_args.kwargs["organization_id"], views.CUSTOMER_ORGANIZATION_ID)
7272
self.assertNotIn("provider", call_args.kwargs)
7373

7474
# Verify redirect response
@@ -85,7 +85,7 @@ def test_auth_provider_login(self):
8585
mock_client = MagicMock()
8686
mock_client.sso = mock_sso
8787

88-
with patch.object(views.workos, "client", mock_client):
88+
with patch.object(views, "workos_client", mock_client):
8989
response = self.client.post(
9090
reverse("auth"),
9191
{"login_method": "google"},
@@ -107,14 +107,15 @@ def test_auth_provider_login(self):
107107

108108
def test_auth_callback_success(self):
109109
"""Test auth_callback view with valid code"""
110-
# Mock the profile response
110+
# Mock the profile response - in SDK v5+, ProfileAndToken uses .dict() method
111111
mock_profile = MagicMock()
112-
mock_profile.to_dict.return_value = {
112+
mock_profile.dict.return_value = {
113113
"profile": {
114114
"first_name": "John",
115115
"last_name": "Doe",
116116
"email": "john.doe@example.com"
117-
}
117+
},
118+
"access_token": "test_token"
118119
}
119120

120121
# Create a mock sso object
@@ -125,7 +126,7 @@ def test_auth_callback_success(self):
125126
mock_client = MagicMock()
126127
mock_client.sso = mock_sso
127128

128-
with patch.object(views.workos, "client", mock_client):
129+
with patch.object(views, "workos_client", mock_client):
129130
response = self.client.get(
130131
reverse("auth_callback"),
131132
{"code": "test_auth_code"},
@@ -154,10 +155,13 @@ def test_auth_callback_missing_code(self):
154155
mock_client = MagicMock()
155156
mock_client.sso = mock_sso
156157

157-
# This should raise a KeyError or return an error
158-
with patch.object(views.workos, "client", mock_client):
159-
with self.assertRaises(KeyError):
160-
self.client.get(reverse("auth_callback"))
158+
# This should render login page with error message (not raise KeyError)
159+
with patch.object(views, "workos_client", mock_client):
160+
response = self.client.get(reverse("auth_callback"))
161+
self.assertEqual(response.status_code, 200)
162+
self.assertTemplateUsed(response, "sso/login.html")
163+
self.assertIn("error", response.context)
164+
self.assertEqual(response.context["error"], "missing_code")
161165

162166
def test_logout(self):
163167
"""Test logout view clears session and redirects"""

python-django-sso-example/sso/views.py

Lines changed: 107 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,62 @@
11
import os
2-
import workos
2+
from workos import WorkOSClient
33
import json
44
from django.conf import settings
55
from django.shortcuts import redirect, render
66
from django.urls import reverse
7+
from pathlib import Path
8+
from dotenv import load_dotenv
9+
10+
# Load environment variables from .env file if it exists
11+
# BASE_DIR is the project root (where manage.py is located)
12+
# views.py is at: python-django-sso-example/sso/views.py
13+
# So we need to go up 2 levels to get to python-django-sso-example/
14+
BASE_DIR = Path(__file__).resolve().parent.parent
15+
env_path = BASE_DIR / ".env"
16+
load_dotenv(env_path, override=False) # Don't override existing env vars
17+
18+
19+
# Initialize WorkOS client
20+
# Note: In SDK v5+, we use WorkOSClient instance instead of workos.client module
21+
def get_workos_client():
22+
"""Get WorkOS client instance (initialized lazily)"""
23+
if not hasattr(get_workos_client, '_instance'):
24+
# Reload .env file in case it wasn't loaded at import time
25+
load_dotenv(env_path, override=False)
26+
27+
api_key = os.getenv("WORKOS_API_KEY")
28+
client_id = os.getenv("WORKOS_CLIENT_ID")
29+
if not api_key or not client_id:
30+
raise ValueError(
31+
"WorkOS API key and client ID must be set via WORKOS_API_KEY and WORKOS_CLIENT_ID environment variables. "
32+
"Please check your .env file or export these variables."
33+
)
34+
get_workos_client._instance = WorkOSClient(
35+
api_key=api_key,
36+
client_id=client_id
37+
)
38+
return get_workos_client._instance
39+
40+
# For compatibility with other examples, create workos_client variable
41+
# Initialize it if env vars are available, otherwise it will be created on first use
42+
try:
43+
if os.getenv("WORKOS_API_KEY") and os.getenv("WORKOS_CLIENT_ID"):
44+
workos_client = WorkOSClient(
45+
api_key=os.getenv("WORKOS_API_KEY"),
46+
client_id=os.getenv("WORKOS_CLIENT_ID")
47+
)
48+
else:
49+
workos_client = None
50+
except ValueError:
51+
# If env vars aren't set at import time, use lazy initialization
52+
workos_client = None
753

8-
9-
workos.api_key = os.getenv("WORKOS_API_KEY")
10-
workos.client_id = os.getenv("WORKOS_CLIENT_ID")
11-
12-
# In workos_django/settings.py, you can use DEBUG=True for local development,
13-
# but you must use DEBUG=False in order to test the full authentication flow
14-
# with the WorkOS API.
54+
# Set custom API base URL for local development
1555
if settings.DEBUG:
1656
os.environ["WORKOS_API_BASE_URL"] = "http://localhost:8000/"
1757

1858
# Constants
19-
# Required: Fill in CUSTOMER_ORGANIZATION_ID for the desired organization from the WorkOS Dashboard
20-
21-
CUSTOMER_ORGANIZATION_ID = "xxx"
59+
CUSTOMER_ORGANIZATION_ID = os.getenv("CUSTOMER_ORGANIZATION_ID")
2260
REDIRECT_URI = os.getenv("REDIRECT_URI")
2361

2462

@@ -39,29 +77,77 @@ def login(request):
3977

4078

4179
def auth(request):
80+
if not REDIRECT_URI:
81+
return render(
82+
request,
83+
"sso/login.html",
84+
{"error": "configuration_error", "error_description": "REDIRECT_URI is not configured"},
85+
)
86+
87+
login_type = request.POST.get("login_method")
88+
if not login_type:
89+
return render(
90+
request,
91+
"sso/login.html",
92+
{"error": "missing_login_method", "error_description": "Login method is required"},
93+
)
4294

43-
login_type = request.POST["login_method"]
4495
params = {"redirect_uri": REDIRECT_URI, "state": {}}
4596

4697
if login_type == "saml":
98+
if not CUSTOMER_ORGANIZATION_ID:
99+
return render(
100+
request,
101+
"sso/login.html",
102+
{"error": "configuration_error", "error_description": "CUSTOMER_ORGANIZATION_ID is not configured"},
103+
)
47104
params["organization_id"] = CUSTOMER_ORGANIZATION_ID
48105
else:
49106
params["provider"] = login_type
50107

51-
authorization_url = workos.client.sso.get_authorization_url(**params)
108+
client = workos_client if workos_client else get_workos_client()
109+
authorization_url = client.sso.get_authorization_url(**params)
52110

53111
return redirect(authorization_url)
54112

55113

56114
def auth_callback(request):
57-
code = request.GET["code"]
58-
profile = workos.client.sso.get_profile_and_token(code)
59-
p_profile = profile.to_dict()
60-
request.session["p_profile"] = p_profile
61-
request.session["first_name"] = p_profile["profile"]["first_name"]
62-
request.session["raw_profile"] = p_profile["profile"]
63-
request.session["session_active"] = True
64-
return redirect("login")
115+
# Check for error response from WorkOS
116+
if "error" in request.GET:
117+
error = request.GET.get("error")
118+
error_description = request.GET.get("error_description", "An error occurred during authentication")
119+
# Log the error and redirect back to login with error message
120+
return render(
121+
request,
122+
"sso/login.html",
123+
{"error": error, "error_description": error_description},
124+
)
125+
126+
# Get the authorization code
127+
code = request.GET.get("code")
128+
if not code:
129+
return render(
130+
request,
131+
"sso/login.html",
132+
{"error": "missing_code", "error_description": "No authorization code received"},
133+
)
134+
135+
try:
136+
client = workos_client if workos_client else get_workos_client()
137+
profile = client.sso.get_profile_and_token(code)
138+
# In SDK v5+, ProfileAndToken is a Pydantic model - use .dict() to convert to dict
139+
p_profile = profile.dict()
140+
request.session["p_profile"] = p_profile
141+
request.session["first_name"] = p_profile["profile"]["first_name"]
142+
request.session["raw_profile"] = p_profile["profile"]
143+
request.session["session_active"] = True
144+
return redirect("login")
145+
except Exception as e:
146+
return render(
147+
request,
148+
"sso/login.html",
149+
{"error": "authentication_error", "error_description": str(e)},
150+
)
65151

66152

67153
def logout(request):

python-django-sso-example/workos_django/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
https://docs.djangoproject.com/en/3.1/howto/static-files/#configuring-static-files
3434
"""
3535
DEBUG = False
36-
# DEBUG = True
3736

3837
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
3938

0 commit comments

Comments
 (0)