Skip to content

81 refactor i uzupełnienie swaggera do metod auth#174

Open
JanekDr wants to merge 15 commits intodevfrom
81-refactor-i-uzupełnienie-swaggera-do-metod-auth

Hidden character warning

The head ref may contain hidden characters: "81-refactor-i-uzupe\u0142nienie-swaggera-do-metod-auth"
Open

81 refactor i uzupełnienie swaggera do metod auth#174
JanekDr wants to merge 15 commits intodevfrom
81-refactor-i-uzupełnienie-swaggera-do-metod-auth

Conversation

@JanekDr
Copy link
Copy Markdown
Member

@JanekDr JanekDr commented Mar 15, 2026

  • Migracja klas: Wszystkie endpointy Solvro/USOS OAuth przeniesione na DRF APIView/AsyncAPIView dla poprawnego generowania schematu. Wszelkie opcje redirect korzystają z parametryzacji docelowych widoków klasowych.
  • Opisy wejść/wyjść: Dodano dekoratory @extend_schema ze szczegółowym wykazem rzucanych kodów błędów (200, 302, 400, 401, 403, 404) dla każdej ścieżki JWT i OTP.
  • Bugfix walidacji na OTP: Dodano authentication_classes = [] do publicznych widoków OTP i LoginLinkView. Eliminacja błędnych response 401 Unauthorized dla starych sesji.
  • Wszystkie endpointy są dostępne w sekcji Authentication w dokumentacji.

@JanekDr JanekDr self-assigned this Mar 15, 2026
@JanekDr JanekDr requested a review from MoonPrincess06 as a code owner March 15, 2026 17:27
@JanekDr JanekDr linked an issue Mar 15, 2026 that may be closed by this pull request
return HttpResponseForbidden()
return redirect(add_query_params(redirect_url, {"error": "no_email"}))
messages.error(request, "Brak adresu email w profilu użytkownika.")
return redirect(redirect_url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

auth_login(request, user)
return redirect(redirect_url)

await async_auth_login(request, user)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

messages.error(request, f"Twoje konto zostało zablokowane: {user.ban_reason or 'Brak powodu'}")
return redirect(redirect_url)

if not user.is_student_and_not_staff:

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

await async_auth_login(request, user)
return redirect(redirect_url)


Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

In general, the safest approach is to ensure that any user-provided redirect URL is normalized and strictly validated before use. That means:

  • Normalize input (strip whitespace, normalize backslashes, and lowercase schemes/hosts).
  • Reject protocol-relative (//...) and backslash-prefixed (\\...) URLs.
  • For non-absolute paths, only allow safe relative URLs.
  • For absolute URLs, accept only http/https, and only when their origin is in ALLOWED_REDIRECT_ORIGINS, matches the current host, or matches a carefully defined preview pattern.
  • Fall back to a safe default path when validation fails.

The current helper already aims to do this, but we can tighten and clarify it:

  1. Normalize the input early:
    • Strip leading/trailing whitespace.
    • Replace \ with / before parsing, to avoid browser/backslash quirks.
  2. Use a single urlparse call and re-use its result; avoid parsing different string variants.
  3. Treat URLs without a scheme but with a netloc-like prefix (e.g., example.com/path) as unsafe, even though browsers can resolve them.
  4. For relative URLs, rely on Django’s url_has_allowed_host_and_scheme with allowed_hosts={request.get_host(), *parsed allowlist hosts} and require_https as configured.
  5. For absolute URLs, compare normalized origins (scheme://netloc lowercased and stripped of trailing slashes) against ALLOWED_REDIRECT_ORIGINS similarly normalized, and apply the preview regexes to that normalized origin.
  6. Ensure that get_safe_redirect_url can still safely handle the "index" default and whatever URL pattern your code already uses there.

Concretely, we’ll modify:

  • is_safe_redirect_url to:
    • Normalize backslashes to forward slashes before any startswith/urlparse operation.
    • Lowercase the scheme and host in comparisons.
    • Normalize ALLOWED_REDIRECT_ORIGINS to comparable origin strings.
    • Use a single urlparse result and reduce duplication.
  • get_safe_redirect_url stays as is logically, but continues to rely on the improved is_safe_redirect_url; its fallback default remains unchanged to preserve behavior.

No changes are needed to the call site in UsosAuthorizeView; after the sanitizer is strengthened, redirect(redirect_url) will be safe.


Suggested changeset 1
users/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/users/views.py b/users/views.py
--- a/users/views.py
+++ b/users/views.py
@@ -224,50 +224,68 @@
     if url == "admin:index":
         return True
 
-    url = str(url).strip()
+    # Normalize the URL: strip whitespace and normalize backslashes to avoid
+    # browser-specific backslash handling quirks.
+    url = str(url).strip().replace("\\", "/")
 
-    if url.startswith("//") or url.startswith("\\\\"):
+    # Reject protocol-relative URLs like //example.com/...
+    if url.startswith("//"):
         return False
 
+    # Allow simple relative paths (handled by Django as local redirects).
     if url.startswith("/"):
         return True
 
+    try:
+        parsed = urlparse(url)
+    except Exception:
+        return False
+
+    # Build the set of allowed hosts from configured origins and the current request.
     allowed_hosts = {urlparse(origin).netloc for origin in ALLOWED_REDIRECT_ORIGINS}
+    allowed_hosts = {h.lower() for h in allowed_hosts if h}
     if request:
-        allowed_hosts.add(request.get_host())
+        allowed_hosts.add(request.get_host().lower())
 
+    # Let Django perform its own safety checks (handles schemes, hosts, etc.).
     is_django_safe = url_has_allowed_host_and_scheme(
         url,
-        allowed_hosts=allowed_hosts,
+        allowed_hosts=allowed_hosts or None,
         require_https=not getattr(settings, "DEBUG", False),
     )
 
-    try:
-        parsed = urlparse(url)
-        if parsed.scheme not in ("http", "https"):
-            return False
+    # Only http/https are ever acceptable for absolute URLs.
+    if parsed.scheme.lower() not in ("http", "https"):
+        return False
 
-        if not parsed.netloc:
-            return False
+    if not parsed.netloc:
+        # Absolute-looking URL without a netloc (e.g. malformed); treat as unsafe.
+        return False
 
-        url_origin = f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
+    url_origin = f"{parsed.scheme.lower()}://{parsed.netloc.lower()}".rstrip("/")
 
-        if is_django_safe:
-            if url_origin in ALLOWED_REDIRECT_ORIGINS:
-                return True
-            if request and parsed.netloc == request.get_host():
-                return True
+    if is_django_safe:
+        # Normalize configured origins for comparison.
+        normalized_allowed_origins = {
+            urlparse(origin).scheme.lower() + "://" + urlparse(origin).netloc.lower()
+            for origin in ALLOWED_REDIRECT_ORIGINS
+            if urlparse(origin).scheme and urlparse(origin).netloc
+        }
+        normalized_allowed_origins = {o.rstrip("/") for o in normalized_allowed_origins}
 
-        if ALLOW_PREVIEW_ENVIRONMENTS:
-            for regex in PREVIEW_ORIGIN_REGEXES:
-                if re.match(regex, url_origin):
-                    return True
+        if url_origin in normalized_allowed_origins:
+            return True
+        if request and parsed.netloc.lower() == request.get_host().lower():
+            return True
 
-        return False
-    except Exception:
-        return False
+    if ALLOW_PREVIEW_ENVIRONMENTS:
+        for regex in PREVIEW_ORIGIN_REGEXES:
+            if re.match(regex, url_origin):
+                return True
 
+    return False
 
+
 def get_safe_redirect_url(url: str, request=None, default="index") -> str:
     if not url:
         return default
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
@JanekDr JanekDr requested a review from MoonPrincess06 March 18, 2026 14:28
@JanekDr JanekDr requested a review from MoonPrincess06 March 29, 2026 18:13
@Antoni-Czaplicki
Copy link
Copy Markdown
Member

@JanekDr co myślisz żeby zrobić helper safe_redirect który w sobie sprawdzi czy jest safe i jak coś to nadpisze?

@Antoni-Czaplicki
Copy link
Copy Markdown
Member

i pamiętaj też żeby dać Resolve jak już jest komentarz ogarnięty

@JanekDr
Copy link
Copy Markdown
Member Author

JanekDr commented Mar 31, 2026

@JanekDr co myślisz żeby zrobić helper safe_redirect który w sobie sprawdzi czy jest safe i jak coś to nadpisze?

Myśle ze to dobry pomysł i uszczupli lekko kod w metodach auth dla usosa i solvro

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the authentication-related OAuth and OTP endpoints in users to improve DRF Spectacular (Swagger/OpenAPI) schema generation, while tightening redirect safety checks and documenting request/response variants.

Changes:

  • Migrates Solvro/USOS OAuth endpoints from function-based views to DRF APIView / adrf async API views.
  • Expands @extend_schema documentation across JWT, OAuth, and OTP endpoints (params, examples, and response codes).
  • Updates redirect safety logic and adds tests for DEBUG-dependent HTTP/HTTPS redirect handling; adds initial OAuth flow tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
users/views.py New class-based OAuth endpoints, enhanced schema docs, updated redirect safety logic, and public OTP views made explicitly unauthenticated.
users/urls.py Routes updated to point to the new class-based OAuth views.
users/tests/test_redirect_url.py Adds DEBUG=True/False coverage for HTTP vs HTTPS redirect acceptance.
users/tests/test_oauth.py Adds tests asserting callback URL composition and a mocked Solvro authorize flow.
Comments suppressed due to low confidence (1)

users/views.py:574

  • Same add_query_params + view-name issue in the banned-user jwt=true branch: if redirect_url is the default view name, redirect(add_query_params(...)) can raise NoReverseMatch and return 500 instead of redirecting with an error.
            if request.GET.get("jwt", "false") == "true":
                auth_params = {"error": "user_banned"}
                if user.ban_reason:
                    auth_params["ban_reason"] = user.ban_reason
                return redirect(add_query_params(redirect_url, auth_params))

Comment on lines +426 to +430
"USOS credentials not configured. USOS_CONSUMER_KEY=%s, USOS_CONSUMER_SECRET=%s",
"<set>" if usos_key else "<missing>",
"<set>" if usos_secret else "<missing>",
)
return redirect(add_query_params(redirect_url, {"error": "usos_unavailable"}))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirect_url is allowed to be empty here (missing/blocked redirect param). In that case add_query_params(redirect_url, ...) will produce a string like ?error=..., and django.shortcuts.redirect() will raise NoReverseMatch because it treats it as a view name (no / or .). Consider falling back to a concrete URL/path (e.g., resolved index URL) when redirect_url is empty, or return a 400 response instead of attempting to redirect.

Copilot uses AI. Check for mistakes.
Comment on lines +464 to +470
if attempt < max_retries - 1:
await sleep(retry_delay)
continue

return redirect(add_query_params(redirect_url, {"error": "usos_unavailable"}))

return redirect(add_query_params(redirect_url, {"error": "usos_unavailable"}))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: on the final retry, redirect_url may still be empty, and redirect(add_query_params(redirect_url, ...)) can crash with NoReverseMatch. Handle the empty/invalid redirect case explicitly before redirecting with query params.

Copilot uses AI. Check for mistakes.
302: OpenApiResponse(
description="Redirect to USOS OAuth provider, or redirect with `?error=usos_unavailable` on failure."
),
400: OpenApiResponse(description="Invalid redirect URL."),
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAPI schema advertises a 400 "Invalid redirect URL" response, but this view no longer returns 400 for an invalid redirect value (it gets replaced by the default in get_safe_redirect_url). Either reintroduce an explicit 400 for invalid redirects or remove/adjust the documented 400 so the schema matches runtime behavior.

Suggested change
400: OpenApiResponse(description="Invalid redirect URL."),

Copilot uses AI. Check for mistakes.
Comment on lines +550 to +554
if not profile.get("email"):
logger.error("Solvro user profile missing email. Profile keys: %s", list(profile.keys()))
if request.GET.get("jwt", "false") == "true":
return redirect(add_query_params(redirect_url, {"error": "authorization_failed"}))
return HttpResponseForbidden()
return redirect(add_query_params(redirect_url, {"error": "no_email"}))
messages.error(request, "Brak adresu email w profilu użytkownika.")
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the jwt=true path, if redirect is missing/blocked then redirect_url falls back to the view name index. Calling add_query_params('index', ...) produces a string like index?error=no_email, and redirect() will raise NoReverseMatch (500) because it treats that as a view name. Ensure redirect_url is a real URL/path before appending query params (e.g., resolve the view name first or use a path default like /).

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +15
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework.test import APIClient

from users.models import User


@override_settings(DEBUG=True)
class SolvroOAuthTests(TestCase):
def setUp(self):
self.client = APIClient()
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are API endpoint tests but they use django.test.TestCase + APIClient. Elsewhere in this repo, API tests consistently use rest_framework.test.APITestCase (e.g., users/tests/test_jwt.py, users/tests/test_guest.py), which provides better DRF integration and consistent helpers. Consider switching this test class to APITestCase for consistency.

Suggested change
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework.test import APIClient
from users.models import User
@override_settings(DEBUG=True)
class SolvroOAuthTests(TestCase):
def setUp(self):
self.client = APIClient()
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APITestCase
from users.models import User
@override_settings(DEBUG=True)
class SolvroOAuthTests(APITestCase):

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor i uzupełnienie swaggera do metod auth

5 participants