Add Hanko SSO Authentication#492
Conversation
Auth functionality only, deploy config in separate PR.
omranlm
left a comment
There was a problem hiding this comment.
@jeafreezy can you pelase review all frontend changes and make sure all clear .. feel free to add comments on specific code lines and tag the submitters for clarification. @emi420 is also HOT team member and has been coordinating those efforts
@kshitijrajsharma and the back end review for you please
Got it. I actually had a first review last week and It looks very great to me. I’d give it a second look over the weekend. |
backend/fairproject/settings.py
Outdated
| # Authentication: "legacy" (OSM OAuth) or "hanko" (Hanko SSO) | ||
| AUTH_PROVIDER = env("AUTH_PROVIDER", default="legacy") | ||
|
|
||
| if AUTH_PROVIDER == "hanko": |
There was a problem hiding this comment.
better to use type safety and the options specially when its string ! or make accepted values constant
There was a problem hiding this comment.
good point, I'll add AuthProvider constants in settings.py
backend/fairproject/urls.py
Outdated
| ) | ||
|
|
||
| admin_mapping_patterns = [] | ||
| if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko': |
There was a problem hiding this comment.
Auth provider should be compulsory ! It should be provided legacy by default yes but assume value needs to be there so getattr is not needed
There was a problem hiding this comment.
makes sense, I'll simplify to settings.AUTH_PROVIDER without getattr
backend/fairproject/urls.py
Outdated
| user_name_column="username", | ||
| user_email_column="email", | ||
| ) | ||
| except ImportError: |
There was a problem hiding this comment.
if the authprovider is hanko it should not pass when package not found ! hotosmauth_django needs to be installed !
There was a problem hiding this comment.
good catch, removed the try/except so it fails if package is missing
| path("api/v1/auth/", include("login.urls")), | ||
| path("api/v1/", include("core.urls")), | ||
| path("api/admin/", admin.site.urls), | ||
| path("api/admin/", include(admin_mapping_patterns)), |
There was a problem hiding this comment.
why ? what is api admin mapping patterns ?
There was a problem hiding this comment.
these are admin endpoints from hotosm-auth for managing hanko-to-OSM user mappings. The login repo has an admin dashboard that communicates with these endpoints in all apps (fAIr, portal, drone-tm, etc.) to view/delete mappings. The mappings live in each app's DB, not in login.
| raise exceptions.AuthenticationFailed( | ||
| "OSM authentication failed: Invalid or expired access token" | ||
| ) | ||
| return (user, None) # authentication successful return id,user_name,img |
There was a problem hiding this comment.
no need to remove the inline comments ?
backend/login/authentication.py
Outdated
| from hotosm_auth_django import get_mapped_user_id | ||
|
|
||
| if not hasattr(request, 'hotosm'): | ||
| logger.debug("No hotosm attribute on request - HankoAuthMiddleware not active") |
There was a problem hiding this comment.
is it possible for this class to be instantiated when hanko is not enabled ? if no then this check should be explicit !
There was a problem hiding this comment.
you're right, HankoAuthentication only runs when Hanko is enabled so the middleware must be there. Changed to raise an explicit error!
backend/login/authentication.py
Outdated
|
|
||
|
|
||
| # Select authentication class based on AUTH_PROVIDER | ||
| if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko': |
There was a problem hiding this comment.
same typed constant here as well that will be easy on the checks astoo to know which auth backend is active rather than string comparison
There was a problem hiding this comment.
already done in the AuthProvider constants fix
| existing = OsmUser.objects.get(osm_id=osm_id) | ||
| logger.info(f"Found existing OsmUser: osm_id={osm_id}") | ||
| return existing | ||
| except OsmUser.DoesNotExist: |
There was a problem hiding this comment.
if else should suffice but this is okay too
| def get_queryset(self): | ||
| queryset = super().get_queryset() | ||
|
|
||
| mine_param = self.request.query_params.get('mine', '').lower() |
There was a problem hiding this comment.
I don't understand this what's mine param ?
There was a problem hiding this comment.
this "mine" param is a workaround we added because there was a bug that didn't allow filtering datasets/models by user directly
How the auth flow works:
- Portal sends request with Hanko cookie to fAIr
- HankoAuthMiddleware extracts the Hanko user from JWT
- HankoAuthentication looks up the user mapping (Hanko ID → OSM ID) in the DB
- Sets request.user to the mapped OsmUser
The problem: ViewSets don't auto-filter by request.user. Your filterset_fields = ["user"] fix allows ?user=<osm_id>, but Portal only has the Hanko token, it doesn't know the osm_id.
The proper fix would be auto-filtering in get_queryset() when user is authenticated. For now mine=true triggers this filter using the resolved request.user. Let me know if you want us to implement the ViewSet fix instead.
backend/login/urls.py
Outdated
| name="request-email-verification", | ||
| ), | ||
| path("me/verify-email/", views.VerifyEmail.as_view(), name="verify-email"), | ||
| # Hanko onboarding endpoint (only used when AUTH_PROVIDER=hanko) |
There was a problem hiding this comment.
then path should only be added when authprovider backend is hanko !
There was a problem hiding this comment.
yes! moved the onboarding path inside an if settings.AUTH_PROVIDER == AuthProvider.HANKO block
backend/login/views.py
Outdated
| app_name="fair", | ||
| ) | ||
|
|
||
| frontend_url = getattr(settings, 'FRONTEND_URL', '/') |
There was a problem hiding this comment.
settings.frontend_url , it has default value so it would be available in django settings
fAIr/backend/fairproject/settings.py
Line 35 in 8cc9a35
There was a problem hiding this comment.
right, simplified to settings.FRONTEND_URL
backend/login/views.py
Outdated
|
|
||
| if not existing_user: | ||
| from urllib.parse import urlencode | ||
| login_url = getattr(settings, 'LOGIN_URL', 'https://login.hotosm.org') |
There was a problem hiding this comment.
same , lets make it explicit defautl value in settings !
There was a problem hiding this comment.
done, simplified to settings.LOGIN_URL
| from hotosm_auth_django import get_mapped_user_id | ||
|
|
||
| if getattr(settings, 'AUTH_PROVIDER', 'legacy') != 'hanko': | ||
| return Response({ |
There was a problem hiding this comment.
Use exceptions https://github.com/hotosm/fAIr/blob/develop/backend/core/exceptions.py You can have a class for login !
There was a problem hiding this comment.
perfect! Added LoginException in core/exceptions.py and using it in OnboardingCallback for the Hanko-specific errors. The responses in AuthStatus are status info (not errors), so they stay as regular responses, returning authenticated: False is expected behavior, not an error
| description = "AI Assisted Mapping" | ||
| readme = "README.md" | ||
| requires-python = ">=3.9" | ||
| requires-python = ">=3.10" |
There was a problem hiding this comment.
hotosm-auth[django] requires python 3.10 I assume ?
There was a problem hiding this comment.
yes, we bumped from 3.9 to 3.10 because hotosm-auth requires it. If needed we could update the lib to support 3.9, but if it's not a problem we'd prefer to keep 3.10
There was a problem hiding this comment.
Kindly remove this from the PR , rather its okay to include it in the description , if you are comfortable with .md you can put this in gist and share the link !
There was a problem hiding this comment.
Can we rename this to hamburger-icon so it's consistent with existing naming convention ?
| {SHARED_CONTENT.navbar.loginButton} | ||
| </Button> | ||
| )} | ||
| <hotosm-tool-menu></hotosm-tool-menu> |
There was a problem hiding this comment.
- Must this always exist even in
legacymode ? It doesn't style well in legacy mode.
But when i switch to Hanko, it does style well.
-
Also, is it possible to detect the current application and disable it from the menu ? It can be also be an argument here https://github.com/hotosm/portal/tree/main/frontend/web-components/tool-menu to make it flexible e.g
disable-active-app, that way the users won't need to see fAIr again on the tools since they're already on fAIr. This just an idea. -
Finally, redirection doesn't work in development environment when i switch to
hankomode. It just takes me to my profile page. See attached video for reference.
Screen.Recording.2026-03-27.at.20.35.42.mov
There was a problem hiding this comment.
thanks for the detailed feedback!
- Tool menu in legacy mode. For now I'm hiding it in legacy mode. The issue is that hanko-auth injects HOT design system CSS globally, which tool-menu depends on. In legacy mode that CSS never loads so the component doesn't render correctly. @warmijusti can you take a look? Not sure if the fix should be in tool-menu (inject CSS globally like hanko-auth does) or if we should load it in fAIr.
- disable-active-app attribute: good idea! @warmijusti thoughts?
- Redirection not working: I saw you tested locally (127.0.0.1) against dev.login. That won't work because Hanko auth shares cookies by subdomain, so dev.login.hotosm.org can't set cookies for localhost. For local dev it's easier to use legacy mode. To test Hanko properly we'd need to deploy to fair-dev where the subdomains align and env vars will be configured correctly.
There was a problem hiding this comment.
Hi everyone, for point number 2, I think it's better to not disable or hide the tool.
- It's good for the user to see in which category the tool is located (imagery, mapping, field, data)
- It will help to locate the app in the list the next time
- Disabling the link is not adding anything to the UI/UX and it prevents users to click it. Some people could use this link
There was a problem hiding this comment.
hi! regarding point number 1: thanks for the catch! version 0.2.7 of the tool menu should solve this issue.
Add Hanko SSO Authentication
Summary
Integrates Hanko SSO authentication as an alternative to legacy OSM OAuth, enabling single sign-on across the HOT ecosystem via login.hotosm.org.
Key changes:
AUTH_PROVIDERsetting to switch betweenlegacy(current) andhankomodesThis PR is functionality only. Deploy configuration (Dockerfiles, workflows, nginx) comes in a separate PR.
For Deployment: Required Secrets & Variables
Backend Environment Variables
When deploying with
AUTH_PROVIDER=hanko:AUTH_PROVIDERhankohankoto enable SSOHANKO_API_URLhttps://login.hotosm.orgCOOKIE_SECRET<shared-secret>COOKIE_DOMAIN.hotosm.orgLOGIN_URLhttps://login.hotosm.orgFRONTEND_URLhttps://fair.hotosm.orgFrontend Environment Variables
VITE_AUTH_PROVIDERhankoVITE_HANKO_URLhttps://login.hotosm.orgImportant Notes
COOKIE_SECRETmust be shared with the login service (login.hotosm.org) - coordinate with login teamCOOKIE_DOMAINshould be.hotosm.orgfor production so cookies work across subdomainslegacymode - existing deployments continue working without changeshotosm_auth_djangoapp (with migrations) only loads whenAUTH_PROVIDER=hankoNew Dependencies
hotosm-auth[django]==0.2.10@hotosm/hanko-authNew API Endpoints
/api/v1/auth/onboarding//api/v1/auth/status/How it Works
Legacy Mode (default)
No changes - continues using OSM OAuth with access-token header.
Hanko Mode
Onboarding Flow
New Hanko users choose:
Test Plan
AUTH_PROVIDER=legacy)?mine=truefilter works for both auth typesBackward Compatibility
legacy- no action needed for existing deploymentshankowhen ready by setting environment variables