django-helsinki-suomifi-on-behalf
Suomi.fi on-behalf (eAuthorizations / "Valtuudet") integration helpers for City of Helsinki Django apps.
This library packages the Suomi.fi eAuthorizations (Valtuudet) on-behalf client flow and the supporting utilities so they can be reused across services instead of being copy-pasted.
Installation
pip install django-helsinki-suomifi-on-behalf
The package depends on django and requests, and supports Python 3.10 and later
with Django 5.2 and 6.0.
There is no app config to register: suomifi_on_behalf does not need to be added to
INSTALLED_APPS. Wire its URLs (see Quickstart) and connect any signals
from your own app.
Quickstart
-
Wire the URLs. Include the library's URLconf; it provides the
eauth_authentication_initandeauth_authentication_callbackroute names:urlpatterns = [ path("", include("suomifi_on_behalf.urls")), ]
Register the callback URL with Suomi.fi as the redirect URI (it is always sent as
https://). -
Set the required settings. The minimum needed to run the flow:
# Issued during Suomi.fi Valtuudet onboarding. SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_BASE_URL = "https://asiointivaltuustarkistus.suomi.fi" SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_CLIENT_ID = "..." SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_CLIENT_SECRET = "..." SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_API_OAUTH_SECRET = "..." # At least one SSN resolver so the flow can obtain the user's hetu. SUOMIFI_ON_BEHALF_SSN_RESOLVERS = [ "suomifi_on_behalf.ssn.OidcUserinfoSsnResolver", ] SUOMIFI_ON_BEHALF_OIDC_USERINFO_ENDPOINT = "https://tunnistus.example.test/openid/userinfo" # Where the user lands after success and after failure. SUOMIFI_ON_BEHALF_LOGIN_SUCCESS_URL = "https://frontend.example.test/success" SUOMIFI_ON_BEHALF_LOGIN_ERROR_URL = "https://frontend.example.test/failure"
See the Settings reference for the full list, defaults, and optional knobs.
-
Start the flow. Send an already authenticated user (see Concepts) to the
eauth_authentication_initroute to begin.
For the end-to-end walkthrough, see docs/eauth-flow.md.
Concepts
On-behalf / mandate model. The flow authenticates a person and then asks Suomi.fi
Valtuudet which organization that person may act on behalf of. The result - the
"organization roles" or mandate - carries the acting company's business id, name, and
the Suomi.fi role codes the person holds. It is stored in
request.session["organization_roles"].
You bring your own login. This library does not log the user in. It builds on an
existing OIDC login that your app provides (no OIDC or SAML backend ships here - use, for
example, mozilla-django-oidc). The flow reads one session key from that login,
request.session["oidc_access_token"], which the built-in SSN resolvers use to obtain
the hetu. If it is missing when the init view runs, SSN resolution fails and the user is
redirected to the error page.
Only one organization role is kept. Only the first organization role from the
API response is stored. If a person holds mandates for several organizations, this
library keeps just one; there is no built-in "switch company" flow. The library confirms
the user holds a mandate for the selected company but does not filter by a specific
mandate/authorization type - inspect the roles list and enforce any specific mandate in
your own code.
What is included
| Module | Purpose |
|---|---|
suomifi_on_behalf.views |
eAuthorizations request/callback views |
suomifi_on_behalf.ssn |
Pluggable SSN (hetu) resolvers |
suomifi_on_behalf.company |
Pluggable company data resolvers |
suomifi_on_behalf.client |
Checksum and Valtuudet / OIDC userinfo HTTP calls |
suomifi_on_behalf.sessions |
Session token storage and redirect helpers |
suomifi_on_behalf.signals |
suomifi_mandate_queried / suomifi_mandate_query_failed |
suomifi_on_behalf.helsinki_profile |
Helsinki Profile GraphQL client (SSN source) |
suomifi_on_behalf.app_settings |
Namespaced access to the library's settings |
The most commonly used names are re-exported from the package root, e.g.
from suomifi_on_behalf import OidcUserinfoSsnResolver, SsnResolutionError.
Guides
- docs/eauth-flow.md - wiring the eAuthorizations flow and SSN resolution end to end (start here).
- docs/company-data.md - fetching and persisting company data.
Reference
Settings
All of this library's own settings are prefixed SUOMIFI_ON_BEHALF_ and are read through
suomifi_on_behalf.app_settings. The standard Django setting LANGUAGE_COOKIE_NAME is
used directly.
Default column: required means the eAuthorizations flow itself fails
(ImproperlyConfigured at startup, or a runtime failure) if it is left unset.
required if using X means it only matters when resolver X is the one you configured.
required to use Y means it is only read by the opt-in helper Y, never by the flow
itself. Any other value shown is the actual default.
eAuthorizations (Valtuudet) credentials
Issued during Suomi.fi Valtuudet onboarding. The checksum and token flows are described in the Suomi.fi documentation (Finnish only): the checksum calculation in palveluhallinta artikkeli 5a781dc7 and the overall Web API flow in palveluhallinta artikkeli 592d7745.
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_BASE_URL |
required | Valtuudet API base URL. Use the Suomi.fi / DVV test base in non-production. |
SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_CLIENT_ID |
required | Client id. |
SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_CLIENT_SECRET |
required | HMAC-SHA256 key that signs the checksum header on every Valtuudet API call. |
SUOMIFI_ON_BEHALF_EAUTHORIZATIONS_API_OAUTH_SECRET |
required | HTTP Basic password for the OAuth token exchange (/oauth/token). |
Redirects
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_LOGIN_SUCCESS_URL |
required | Where to send the user after a successful login; the init view raises ImproperlyConfigured if unset. |
SUOMIFI_ON_BEHALF_LOGIN_ERROR_URL |
required | Where to send the user when authorization fails. |
SUOMIFI_ON_BEHALF_REDIRECT_ALLOWED_HOSTS |
[] |
Extra hostnames allowed for the optional per-login destination stored in request.session["eauth_next_url"] (the request's own host is always allowed). |
SUOMIFI_ON_BEHALF_REDIRECT_REQUIRE_HTTPS |
falls back to request.is_secure() |
Whether that eauth_next_url destination must be HTTPS. |
See Redirects for how eauth_next_url is used.
Language segment. When a LANGUAGE_COOKIE_NAME cookie is present, the success and
failure redirects get the language appended as a path segment: with
SUOMIFI_ON_BEHALF_LOGIN_SUCCESS_URL = "https://frontend.example.test/success" and a
fi cookie the user is sent to https://frontend.example.test/success/fi/. With no
language cookie the URL is used unchanged. Build your frontend routes to expect this.
SSN (hetu) resolver settings
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_SSN_RESOLVERS |
required | Ordered list of dotted paths to SSN resolvers, tried until one succeeds. |
OidcUserinfoSsnResolver
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_OIDC_USERINFO_ENDPOINT |
required if using OidcUserinfoSsnResolver |
OIDC userinfo endpoint read for the hetu claim. |
SUOMIFI_ON_BEHALF_OIDC_VERIFY_SSL |
True |
Verify TLS on the userinfo request. |
SUOMIFI_ON_BEHALF_OIDC_TIMEOUT |
None |
Userinfo request timeout in seconds. |
SUOMIFI_ON_BEHALF_OIDC_PROXY |
None |
Proxy configuration for the userinfo request. |
HelsinkiProfileSsnResolver
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_HELSINKI_PROFILE_API_URL |
required if using HelsinkiProfileSsnResolver |
Helsinki Profile GraphQL URL. |
SUOMIFI_ON_BEHALF_HELSINKI_PROFILE_AUDIENCE |
required if using HelsinkiProfileSsnResolver |
API-token audience. |
SUOMIFI_ON_BEHALF_HELSINKI_PROFILE_SCOPE |
required if using HelsinkiProfileSsnResolver |
API-token scope. |
SUOMIFI_ON_BEHALF_TUNNISTUS_API_TOKENS_ENDPOINT |
required if using HelsinkiProfileSsnResolver |
Tunnistus/Keycloak API-tokens endpoint. |
Company data resolver settings
None of these are read by the eAuthorizations flow itself: they only matter if your
app calls the opt-in get_company() helper.
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_COMPANY_RESOLVERS |
required to use get_company() |
Ordered list of dotted paths to company resolvers, tried until one succeeds. |
SUOMIFI_ON_BEHALF_CACHE_COMPANY_IN_SESSION |
True |
Whether get_company caches the resolved company in request.session["company"]. Set False when your app is the source of truth for company data and re-resolves each request; the session is then never read or written. |
YtjCompanyResolver
| Setting | Default | Purpose |
|---|---|---|
SUOMIFI_ON_BEHALF_YTJ_BASE_URL |
required if using YtjCompanyResolver |
YTJ (avoindata PRH v3) base URL. |
SUOMIFI_ON_BEHALF_YTJ_TIMEOUT |
30 |
YTJ request timeout in seconds. |
SSN (hetu) resolvers
The flow needs the user's national identification number to register and query mandates.
How it is obtained is pluggable via SUOMIFI_ON_BEHALF_SSN_RESOLVERS, a list of dotted
paths tried in order until one succeeds. Each entry is a callable (request) -> str (or
a class instantiated with no arguments and itself callable) that returns the hetu or
raises suomifi_on_behalf.ssn.SsnResolutionError; when every resolver raises, the
request view redirects to SUOMIFI_ON_BEHALF_LOGIN_ERROR_URL.
Built-in resolvers:
OidcUserinfoSsnResolver- reads thenational_id_numclaim fromSUOMIFI_ON_BEHALF_OIDC_USERINFO_ENDPOINT. Subclass and setclaimto match your IdP. Only plain JSON userinfo is supported; a signed-JWT (application/jwt) response is rejected with a clear error, in which case supply a custom resolver that verifies and decodes it.HelsinkiProfileSsnResolver- readsverifiedPersonalInformation.nationalIdentificationNumberfrom the Helsinki Profile GraphQL API.
Writing a custom resolver and the full worked example live in docs/eauth-flow.md.
Company data resolvers
Turning the mandate's business id into fuller company data is pluggable via
SUOMIFI_ON_BEHALF_COMPANY_RESOLVERS, tried in order until one succeeds. Each entry is a
callable (request) -> dict (or a no-arg class that is callable) that returns a company
dict or raises suomifi_on_behalf.company.CompanyResolutionError; when every resolver
raises, get_company re-raises CompanyResolutionError.
Built-in resolvers:
YtjCompanyResolver- looks the company up in the YTJ (avoindata PRH v3) open data API by business id and returns the preferred (Finnish, then Swedish) name, company form, industry and address. RequiresSUOMIFI_ON_BEHALF_YTJ_BASE_URL.OrganizationRolesCompanyResolver- performs no external call and returns only thenameandbusiness_idcarried by the mandate. Intended as the terminal fallback in a chain.
The built-in resolvers return the keys documented above, but get_company does not
validate or enforce a schema: it passes through whatever dict the configured resolver
returns. A custom resolver may therefore return any JSON-serializable dict, including a
superset of the built-in keys (for example an integer company_form_code or a
industry_code string that your own model persists). The effective contract is "whatever
your configured resolver returns," so when chaining resolvers, have them agree on a
superset of keys to give callers a stable shape.
The full worked example (persisting to your own model, a DRF view, a custom resolver, audit signals and tests) lives in docs/company-data.md.
Public API
get_organization_roles(request) returns the cached mandate or performs the HTTP query
on a miss:
from suomifi_on_behalf import get_organization_roles
roles = get_organization_roles(request)
# {"identifier": "0877830-0", "name": "Example Oy", "complete": True, "roles": ["NIMKO"]}
identifier is the company's business id, name its name, and roles a list of
Suomi.fi mandate/role codes (for example NIMKO) the person holds for that company.
get_company(request) returns the cached company dict from the session when present,
otherwise resolves it once from the mandate already in the session, caches it in
request.session["company"], and emits suomifi_company_resolved:
from suomifi_on_behalf import get_company
company = get_company(request)
# {"name": ..., "business_id": ..., "company_form": ..., "industry": ...,
# "street_address": ..., "postcode": ..., "city": ...}
Pass get_company(request, use_cache=False) to skip the session entirely - the resolver
runs and nothing is read from or written to request.session["company"]. Omitting the
argument follows the SUOMIFI_ON_BEHALF_CACHE_COMPANY_IN_SESSION setting (default
True); an explicit use_cache overrides it. Disable caching when your application is
the source of truth for company data and re-resolves on each request. The
suomifi_company_resolved / suomifi_company_resolution_failed signals fire whenever the
resolver runs regardless of caching; a cache hit emits nothing.
This library only returns the data. Persisting a Company record (and its schema) is
left entirely to the consuming application.
Both helpers let requests exceptions (RequestException / HTTPError) propagate and
do not redirect on failure. The redirect-to-error behavior lives only in the callback
view, so wrap these calls accordingly when using them from your own views.
Signals
Connect to the mandate query signals for audit logging:
from suomifi_on_behalf.signals import (
suomifi_mandate_queried,
suomifi_mandate_query_failed,
)
suomifi_mandate_queried is sent with request, request_id and
organization_roles; suomifi_mandate_query_failed is sent with request,
request_id and error.
The company resolution signals are:
from suomifi_on_behalf.signals import (
suomifi_company_resolved,
suomifi_company_resolution_failed,
)
suomifi_company_resolved is sent with request and company;
suomifi_company_resolution_failed is sent with request and error.
Connect receivers to these suomifi_on_behalf.signals objects specifically. They are
distinct Signal instances, not the same objects as any similarly named signals that
may already exist in your project (for example if you migrated incrementally from a
copy-pasted version of this code). Connecting a receiver to a different signal object
silently receives nothing.
Session keys and logout
The flow stores several values in the Django session: eauth_id_token,
eauth_access_token (plus refresh/expiry variants), organization_roles, and the
cached company. The library provides no teardown helper, so clear them yourself on
logout:
for key in (
"eauth_id_token",
"eauth_access_token",
"eauth_access_token_expires",
"eauth_refresh_token",
"eauth_refresh_token_expires",
"organization_roles",
"company",
):
request.session.pop(key, None)
Because these values include access tokens and personal mandate data, avoid the signed-cookie session backend: it exposes the tokens to the client and can exceed the cookie size limit. Use a server-side session backend (database, cache, or file).
Development
hatch test # run the test suite
hatch run lint # run pre-commit hooks
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_helsinki_suomifi_on_behalf-2.0.0.tar.gz.
File metadata
- Download URL: django_helsinki_suomifi_on_behalf-2.0.0.tar.gz
- Upload date:
- Size: 35.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9fcfbbff0724c04719d01097342c057356b95d2d35e5a134ddfd75d6f2d6cac
|
|
| MD5 |
49fb1a9c6a22bd2f7ad73921582cfe86
|
|
| BLAKE2b-256 |
cd6a2cc19faa328a5f850715eefc34ce42fd5bc8041ee4170727dcba076b8807
|
Provenance
The following attestation bundles were made for django_helsinki_suomifi_on_behalf-2.0.0.tar.gz:
Publisher:
publish.yml on City-of-Helsinki/django-helsinki-suomifi-on-behalf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_helsinki_suomifi_on_behalf-2.0.0.tar.gz -
Subject digest:
d9fcfbbff0724c04719d01097342c057356b95d2d35e5a134ddfd75d6f2d6cac - Sigstore transparency entry: 2359936461
- Sigstore integration time:
-
Permalink:
City-of-Helsinki/django-helsinki-suomifi-on-behalf@1e085883d24194cf9c9fc997545d366ae4150ad4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/City-of-Helsinki
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1e085883d24194cf9c9fc997545d366ae4150ad4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_helsinki_suomifi_on_behalf-2.0.0-py3-none-any.whl.
File metadata
- Download URL: django_helsinki_suomifi_on_behalf-2.0.0-py3-none-any.whl
- Upload date:
- Size: 22.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bc4f514c503d2a2e21c1fb864849b4fa0a0baa2b6b54192d7c5da77a48671d1
|
|
| MD5 |
dbaae89367a7d0811a2cf5f3a921b744
|
|
| BLAKE2b-256 |
35414ab759eb6e544b875f12fbd6357ff9616e9782e91d44d4bc7822e6d389cb
|
Provenance
The following attestation bundles were made for django_helsinki_suomifi_on_behalf-2.0.0-py3-none-any.whl:
Publisher:
publish.yml on City-of-Helsinki/django-helsinki-suomifi-on-behalf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_helsinki_suomifi_on_behalf-2.0.0-py3-none-any.whl -
Subject digest:
0bc4f514c503d2a2e21c1fb864849b4fa0a0baa2b6b54192d7c5da77a48671d1 - Sigstore transparency entry: 2359936506
- Sigstore integration time:
-
Permalink:
City-of-Helsinki/django-helsinki-suomifi-on-behalf@1e085883d24194cf9c9fc997545d366ae4150ad4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/City-of-Helsinki
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1e085883d24194cf9c9fc997545d366ae4150ad4 -
Trigger Event:
push
-
Statement type: