Skip to content

Repository files navigation

Ona OpenID Connect Client CI

A pluggable django application that implements OpenID Connect client functionalities.

Installation

  1. Install package using pip:
pip install -e git+https://github.com/onaio/ona-oidc.git#egg=ona-oidc
  1. Add oidc to the list of INSTALLED_APPS
...

INSTALLED_APPS = [
    ...,
    "oidc",
    ...,
]

...
  1. Set OPENID_CONNECT_VIEWSET_CONFIG and OPENID_CONNECT_AUTH_SERVERS settings
...
OPENID_CONNECT_VIEWSET_CONFIG = {
    "JWT_SECRET_KEY": JWT_SECRET_KEY,
    "JWT_ALGORITHM": JWT_ALGORITHM,
    "REQUIRED_USER_CREATION_FIELDS": ["email", "first_name", "username"],
    "USER_CREATION_FIELDS": ["email", "first_name", "last_name", "username"],
    "MAP_CLAIM_TO_MODEL": {
        "given_name": "first_name",
        "family_name": "last_name",
        "preferred_username": "username",
    },
    "SPLIT_NAME_CLAIM": False, # Whether to split the `name` claim into first_name & last_name if present
    "USER_UNIQUE_FILTER_FIELDS": ["username", "email"],
    "USE_SSO_COOKIE": True,
    "SSO_COOKIE_DATA": "email",
    "SSO_COOKIE_MAX_AGE": None,
    "SSO_COOKIE_DOMAIN": "localhost",
    "SSO_COOKIE_PATH": "/",
    "SSO_COOKIE_SECURE": None,  # None => fall back to settings.SESSION_COOKIE_SECURE
    "SSO_COOKIE_SAMESITE": "Lax",  # "Strict" | "Lax" | "None"
    "SSO_COOKIE_HTTPONLY": True,
    "USE_AUTH_BACKEND": False,
    "AUTH_BACKEND": "",  # Defaults to django.contrib.auth.backends.ModelBackend
    "REDIRECT_AFTER_AUTH": "http://localhost:3000",
    "USE_RAPIDPRO_VIEWSET": False,
    "REPLACE_USERNAME_CHARACTERS": "-.",  # A string of characters to replace if found within the captured username when using the `USE_EMAIL_USERNAME` functionality
    "USERNAME_REPLACEMENT_CHARACTER": "_", # The character used to replace the characters within the `REPLACE_USERNAME_CHARACTERS` string
    # A map containing a field as a key and a map containing the regex and optional help_text strings as it's value
    # that's used to validate all field inputs retrieved for the particular key
    "FIELD_VALIDATION_REGEX": {
        "username": {
            "regex": "^(?!\d+$)[a-zA-Z0-9]{3,}$",
            "help_text": "Username should only contain alpha numeric characters",
        }
    },
    # A map containing an optional `default` key along side other regex keys i.e ^.*@ona.io$ with the value being
    # what defaults users with emails that match the regex or don't match any regex(default) should get.
    "USER_DEFAULTS": {
        "default": {
            "is_active": False
        },
        <regex_value>: {
            "is_active": True
        }
    }
}

OPENID_CONNECT_AUTH_SERVERS = {
    "microsoft": {
        "AUTHORIZATION_ENDPOINT": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
        "CLIENT_ID": "client_id",
        "JWKS_ENDPOINT": "https://login.microsoftonline.com/common/discovery/v2.0/keys",
        "SCOPE": "openid profile",
        "TOKEN_ENDPOINT": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
        "END_SESSION_ENDPOINT": "http://localhost:3000",
        "REDIRECT_URI": "http://localhost:8000/oidc/msft/callback",
        "RESPONSE_TYPE": "id_token",
        "RESPONSE_MODE": "form_post",
        "USE_NONCES": True,
        "NONCE_CACHE_TIMEOUT": 1800,
    }
}
...

SSO cookie security

The SSO cookie emitted on successful authentication honours these keys:

  • SSO_COOKIE_SECURE (bool | None, default None). When None, the Secure flag falls back to settings.SESSION_COOKIE_SECURE. Set this to True to force Secure on regardless of the Django session setting, or False to force it off. Production deployments should either set SESSION_COOKIE_SECURE=True (recommended — applies across session/CSRF/SSO cookies) or set SSO_COOKIE_SECURE=True explicitly.
  • SSO_COOKIE_SAMESITE (str, default "Lax"). One of "Strict", "Lax", or "None". "Lax" is the right default for the standard redirect-callback flow. Federated or iframe-embedded OIDC deployments must use "None" + Secure=True — the viewset refuses to start with SameSite="None" while Secure is falsy (Django/browsers reject such cookies anyway).
  • SSO_COOKIE_HTTPONLY (bool, default True). Leave on unless you have a specific reason to expose the cookie to JavaScript.
  • SSO_COOKIE_PATH (str, default "/").

Future work: once Secure=True is enforced cohort-wide, the cookie name can migrate to __Secure-SSO for browser-enforced guarantees.

Forwarding query parameters to the authorization endpoint

The login viewset forwards browser query parameters to the configured authorization endpoint only if they appear in a deployment-defined allowlist on the relevant auth server. The default is an empty list, so no browser query parameters reach the IdP unless explicitly opted in.

OPENID_CONNECT_AUTH_SERVERS = {
    "microsoft": {
        ...,
        "LOGIN_QUERY_PARAM_ALLOWLIST": [
            "prompt",
            "ui_locales",
            "acr_values",
            "login_hint",
        ],
    }
}

next is consumed by ona-oidc for redirect-after-auth caching and is never forwarded, regardless of allowlist. The client-side RESERVED_AUTHORIZE_PARAMS filter (covering OIDC params ona-oidc generates plus JAR / SIOP request-object hooks) applies as a second boundary for any programmatic caller of OpenIDClient.login().

Two encoding details worth knowing:

  • If a caller repeats a key (e.g. ?prompt=login&prompt=consent), only the last value is forwarded. Most IdPs would resolve the duplicate the same way, but if you need to send multiple values for one key, encode them into a single value at the caller.
  • Whitespace and other reserved characters in forwarded values are percent-encoded (e.g. spaces → %20, not +). Both forms are spec-equivalent in URL query strings; ona-oidc picks %20 for log readability.

Validating the post-authentication redirect target (next)

The viewset only accepts a next query parameter that points at a relative path or a host explicitly trusted in OPENID_CONNECT_AUTH_SERVERS[<server>]["LOGIN_REDIRECT_ALLOWED_HOSTS"]. The current request's own host is always trusted, so same-origin deployments need no extra config. Unsafe values are dropped (logged at WARNING) and the post-auth redirect falls back to the configured OPENID_CONNECT_VIEWSET_CONFIG["REDIRECT_AFTER_AUTH"].

OPENID_CONNECT_AUTH_SERVERS = {
    "microsoft": {
        ...,
        "LOGIN_REDIRECT_ALLOWED_HOSTS": ["spa.example.com"],
    }
}

Validation runs Django's url_has_allowed_host_and_scheme, so javascript: / data: schemes and protocol-relative //evil URLs are rejected. Under HTTPS, http:// redirects are also rejected.

next is honored regardless of USE_NONCES. When the caller supplies next, ona-oidc allocates a nonce and caches the validated redirect target under it for the duration of the auth round-trip; the callback restores the value from cache and uses it as the post-auth redirect. With USE_NONCES=False the nonce is still emitted purely as the cache key — no IdP-side nonce verification is performed.

  1. (Optional) If you'd like to use the default OpenID Connect Viewset register the urls located in oidc.urls.
# urls.py file

...
from django.conf.urls import include, url

urlpatterns = [
    ...,
    url(r"^", include("oidc.urls")),
    ...,
]
...

Import User (Optional)

The ona-oidc package includes an optional import user feature that allows administrators to search and import users from an external OIDC provider through the Django admin interface. This feature is useful for organizations that want to create users based on data from their identity provider.

Configuration

To enable the import user feature, add the OPENID_IMPORT_USER setting to your Django settings:

OPENID_IMPORT_USER = {
    "ENABLED": True,  # Set to False to disable the feature
    "TOKEN_ENDPOINT": "https://idp.example.com/oauth/token",  # OAuth2 token endpoint
    "SEARCH_ENDPOINT": "https://idp.example.com/users",  # User search API endpoint
    "CLIENT_ID": "your_client_id",  # OAuth2 client ID
    "CLIENT_SECRET": "your_client_secret",  # OAuth2 client secret
    "SCOPE": "users.read",  # OAuth2 scope for user search
    "QUERY_PARAM": "q",  # Query parameter name for search
    "MAP_CLAIM_TO_MODEL": {  # Maps identity provider claims to user model fields
        "email": "email",
        "given_name": "first_name",
        "family_name": "last_name",
        "preferred_username": "username",
    },
    "SEARCH_RESULTS_PATH": "data.results",  # Optional: JSON path to user list in response
}

Usage

  1. Access the Feature: Navigate to Django Admin → Authentication and Authorization → Users → Add user
  2. Search Users: In the import form, start typing in the search box to find users from your identity provider
  3. Select User: Click on a suggestion to populate the form fields with data from the identity provider
  4. Complete Import: Fill in any additional required fields and save the user

About

A pluggable django application that adds OpenID connect client functionality.

Resources

Stars

3 stars

Watchers

24 watching

Forks

Releases

Packages

Used by

Contributors

Languages