Skip to main content

Authentication & permissions

GreekManage supports four sign-in paths and a layered permission model.

Sign-in methods

JWT

Library: djangorestframework-simplejwt 5.5 Custom auth class: apps.common.authentication.CookieJWTAuthentication

backend/apps/common/authentication.py:7-21
backend/greekmanage/settings.py:248-254

Token lifetimes

TokenLifetimeStorage
Access30 minuteshttpOnly cookie (web) / Authorization: Bearer (mobile)
Refresh24 hourshttpOnly cookie (web) / keychain (mobile)

Rotation + blacklist

Refresh tokens rotate on use — every refresh issues a new refresh + blacklists the old. So a stolen refresh token has a hard ceiling on its useful life:

  • Used legitimately → rotated → previous token blacklisted
  • Used by attacker → previous token blacklisted → real user's next refresh fails → forced re-login

Custom CookieJWTAuthentication

Reads the JWT from the httpOnly cookie access_token first; falls back to Authorization: Bearer <token> for mobile clients that don't speak cookies.

# backend/apps/common/authentication.py
class CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
token = request.COOKIES.get("access_token") \
or self.get_header(request)
if not token:
return None
validated = self.get_validated_token(token)
return self.get_user(validated), validated
HttpOnly: true
Secure: true (in production)
SameSite: Lax
Path: /

Set by set_auth_cookies() in apps/common/authentication.py:27-48.

SAML 2.0

Library: python3-saml 1.16

For enterprise customers with an existing IdP (Okta, Azure AD, OneLogin, Google Workspace SAML, etc.).

backend/apps/authentication/saml.py
backend/apps/authentication/models.py:36-55 (SAMLConfiguration)

Per-tenant config

Each org configures its own SAMLConfiguration with:

  • IdP entity ID + SSO URL + SLO URL
  • IdP x509 cert (encrypted via EncryptedTextField)
  • Attribute mapping (which SAML attributes map to user fields)
  • Auto-provision new users? (yes/no)

Endpoints (per IdP slug)

URLPurpose
/api/auth/sso/saml/<slug>/metadata/SP metadata XML for IdP-side configuration
/api/auth/sso/saml/<slug>/login/Initiates SSO (redirect to IdP)
/api/auth/sso/saml/<slug>/acs/Assertion Consumer Service (POST from IdP)
/api/auth/sso/saml/<slug>/sls/Single Logout Service

Bindings

  • HTTP-POST for assertions (signed XML payload)
  • HTTP-Redirect for logout

Attribute mapping

Default mapping looks up:

  • urn:oid:0.9.2342.19200300.100.1.3email
  • urn:oid:2.5.4.42first_name
  • urn:oid:2.5.4.4last_name

Customers can override per-config.

OAuth 2.0 / OIDC

Library: requests-oauthlib 2.0 Providers: Google Workspace, Microsoft 365, Okta, LinkedIn (LinkedIn used for profile sync, not auth)

backend/apps/authentication/oauth_providers.py
backend/apps/authentication/models.py:57-77 (OAuthConfiguration)

Per-tenant config

Each org configures OAuthConfiguration with:

  • Provider (Google / Microsoft / Okta)
  • Client ID + client secret (secret encrypted)
  • Scopes (defaults: openid email profile)
  • Allowed email domains (e.g., chapter-name.org)

Defaults for Google + Microsoft are pre-baked (authorization / token / userinfo URLs); Okta requires custom URLs.

Discovery

When a user enters their email on the login page, the frontend hits /api/auth/sso/discover?email=.... The backend looks up which orgs have an OAuthConfiguration matching that email's domain and returns the appropriate provider button.

Flow

Email verification

Users can have multiple emails (UserEmail table). Each carries:

  • email (the address)
  • is_verified (bool)
  • verification_token (random token, generated on add)
  • verification_sent_at

SSO flows match incoming verified emails against UserEmail first, then fall back to User.email. This lets users keep their account when they change primary email.

backend/apps/accounts/models.py:76-112 (UserEmail)

Permission classes

All in backend/apps/common/permissions.py (291 lines).

Hierarchy

Class reference

ClassWhat it checksExample endpoint
IsNationalAdminUser is platform admin OR active org adminPOST /api/organizations/<id>/admins/
IsRegionalAdminPlatform admin OR active regional admin (region-scoped)GET /api/regions/<id>/chapters/
IsNationalOrRegionalAdminEither of the aboveGET /api/compliance/region-overview/
IsChapterMemberPlatform admin OR has active membership in any chapterGET /api/chapters/<id>/feed/
IsChapterOfficerPlatform admin OR officer/president in active standingPOST /api/chapters/<id>/bulletins/
IsNationalAdminOrChapterOfficerEither national admin or chapter officer (mixed scope)PATCH /api/memberships/<id>/

App-specific permissions

Some apps have their own:

  • apps/alumni/permissions.pyIsAlumniMember, IsAuthenticatedMember
  • apps/foundation/permissions.pyIsFoundationAdmin, IsFoundationEditor
  • apps/forums/permissions.pyIsForumMemberOrAdmin

Object-level checks

TierPermission provides _get_object_org_id(), _get_object_chapter_id(), _get_object_region_id() helpers. Subclasses use them in has_object_permission() to check that an object belongs to the user's scope.

Example:

class IsChapterOfficer(TierPermission):
def has_object_permission(self, request, view, obj):
if self._is_platform_admin(request.user):
return True
chapter_id = self._get_object_chapter_id(obj)
return chapter_id in self._user_officer_chapter_ids(request.user)

Default permission

REST_FRAMEWORK settings:

"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
)

So every endpoint defaults to "must be signed in." Public endpoints (login, register, public donate, PNM apply) explicitly use permission_classes = [AllowAny].

Throttling

Throttle scopeAnon rateUser rate
Default200/day prod, 10000/day dev2000/day prod, 100000/day dev
auth10/minute prod, 10000/minute dev
sso20/minute prod
account_request3/hour prod
password_reset5/hour prod
pnm_submit_ip5/hour
pnm_submit_email1/minute

Production rates are deliberately tight to prevent enumeration / brute-force.

Mobile auth

Mobile clients (Capacitor) don't use cookies. They send Authorization: Bearer <access_token> and store both tokens in the device keychain (iOS: kSecAttrAccessibleAfterFirstUnlock; Android: EncryptedSharedPreferences).

Biometric unlock uses @aparajita/capacitor-biometric-auth to gate access to the stored tokens.