Skip to content

Add Hanko SSO Authentication#492

Merged
kshitijrajsharma merged 10 commits intodevelopfrom
feature/hanko-auth
Apr 7, 2026
Merged

Add Hanko SSO Authentication#492
kshitijrajsharma merged 10 commits intodevelopfrom
feature/hanko-auth

Conversation

@hg1g
Copy link
Copy Markdown
Collaborator

@hg1g hg1g commented Mar 12, 2026

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:

  • New AUTH_PROVIDER setting to switch between legacy (current) and hanko modes
  • Hanko auth uses JWT cookies instead of access-token headers
  • User onboarding flow to link existing accounts or create new ones
  • Shared auth-libs web component for login UI

This 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:

Variable Required Example Description
AUTH_PROVIDER Yes hanko Set to hanko to enable SSO
HANKO_API_URL Yes https://login.hotosm.org Hanko service URL
COOKIE_SECRET Yes <shared-secret> Must match login service - for cookie encryption
COOKIE_DOMAIN Yes .hotosm.org Domain for auth cookies
LOGIN_URL No https://login.hotosm.org Login service URL for redirects
FRONTEND_URL Yes https://fair.hotosm.org Frontend URL for redirects

Frontend Environment Variables

Variable Required Example Description
VITE_AUTH_PROVIDER Yes hanko Must match backend
VITE_HANKO_URL Yes https://login.hotosm.org Hanko service URL

Important Notes

  1. COOKIE_SECRET must be shared with the login service (login.hotosm.org) - coordinate with login team
  2. COOKIE_DOMAIN should be .hotosm.org for production so cookies work across subdomains
  3. Default is legacy mode - existing deployments continue working without changes
  4. Run migrations after setting env vars - the hotosm_auth_django app (with migrations) only loads when AUTH_PROVIDER=hanko

New Dependencies

Package Location Notes
hotosm-auth[django]==0.2.10 Backend (PyPI) Hanko auth middleware & helpers
@hotosm/hanko-auth Frontend (npm) Login web component

New API Endpoints

Endpoint Method Description
/api/v1/auth/onboarding/ GET Callback from login service after onboarding
/api/v1/auth/status/ GET Check authentication status

How it Works

Legacy Mode (default)

AUTH_PROVIDER=legacy

No changes - continues using OSM OAuth with access-token header.

Hanko Mode

AUTH_PROVIDER=hanko
  1. User clicks login → redirected to login.hotosm.org
  2. Hanko sets JWT cookie after authentication
  3. Backend middleware validates JWT cookie
  4. If user mapping exists → authenticated
  5. If no mapping → user goes through onboarding

Onboarding Flow

New Hanko users choose:

  • "I had an account" → Connect OSM to recover existing fAIr data
  • "I'm new" → Create fresh account with synthetic ID

Test Plan

  • Legacy auth continues working (AUTH_PROVIDER=legacy)
  • Hanko login/logout flow works
  • New user onboarding creates account
  • Existing user onboarding recovers data
  • Navbar shows correct user state
  • Protected routes redirect to login correctly
  • ?mine=true filter works for both auth types

Backward Compatibility

  • Default is legacy - no action needed for existing deployments
  • Existing users continue working with OSM OAuth
  • Can switch to hanko when ready by setting environment variables

hg1g added 2 commits March 12, 2026 19:04
Auth functionality only, deploy config in separate PR.
@emi420 emi420 marked this pull request as ready for review March 13, 2026 03:04
@emi420 emi420 marked this pull request as draft March 13, 2026 03:05
@hg1g hg1g marked this pull request as ready for review March 13, 2026 14:36
@kshitijrajsharma kshitijrajsharma requested review from jeafreezy and kshitijrajsharma and removed request for jeafreezy March 19, 2026 09:34
@omranlm omranlm requested a review from jeafreezy March 27, 2026 15:22
@omranlm omranlm added enhancement New feature or request component : frontend labels Mar 27, 2026
Copy link
Copy Markdown
Contributor

@omranlm omranlm left a comment

Choose a reason for hiding this comment

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

@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

@jeafreezy
Copy link
Copy Markdown
Collaborator

@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.

# Authentication: "legacy" (OSM OAuth) or "hanko" (Hanko SSO)
AUTH_PROVIDER = env("AUTH_PROVIDER", default="legacy")

if AUTH_PROVIDER == "hanko":
Copy link
Copy Markdown
Member

@kshitijrajsharma kshitijrajsharma Mar 27, 2026

Choose a reason for hiding this comment

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

better to use type safety and the options specially when its string ! or make accepted values constant

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

good point, I'll add AuthProvider constants in settings.py

)

admin_mapping_patterns = []
if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

makes sense, I'll simplify to settings.AUTH_PROVIDER without getattr

user_name_column="username",
user_email_column="email",
)
except ImportError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if the authprovider is hanko it should not pass when package not found ! hotosmauth_django needs to be installed !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why ? what is api admin mapping patterns ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no need to remove the inline comments ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

restored!

from hotosm_auth_django import get_mapped_user_id

if not hasattr(request, 'hotosm'):
logger.debug("No hotosm attribute on request - HankoAuthMiddleware not active")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is it possible for this class to be instantiated when hanko is not enabled ? if no then this check should be explicit !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

you're right, HankoAuthentication only runs when Hanko is enabled so the middleware must be there. Changed to raise an explicit error!



# Select authentication class based on AUTH_PROVIDER
if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if else should suffice but this is okay too

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

true, both work fine

def get_queryset(self):
queryset = super().get_queryset()

mine_param = self.request.query_params.get('mine', '').lower()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand this what's mine param ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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:

  1. Portal sends request with Hanko cookie to fAIr
  2. HankoAuthMiddleware extracts the Hanko user from JWT
  3. HankoAuthentication looks up the user mapping (Hanko ID → OSM ID) in the DB
  4. 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.

name="request-email-verification",
),
path("me/verify-email/", views.VerifyEmail.as_view(), name="verify-email"),
# Hanko onboarding endpoint (only used when AUTH_PROVIDER=hanko)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

then path should only be added when authprovider backend is hanko !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes! moved the onboarding path inside an if settings.AUTH_PROVIDER == AuthProvider.HANKO block

app_name="fair",
)

frontend_url = getattr(settings, 'FRONTEND_URL', '/')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

settings.frontend_url , it has default value so it would be available in django settings

FRONTEND_URL = env("FRONTEND_URL", default="https://fair-dev.hotosm.org")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

right, simplified to settings.FRONTEND_URL


if not existing_user:
from urllib.parse import urlencode
login_url = getattr(settings, 'LOGIN_URL', 'https://login.hotosm.org')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same , lets make it explicit defautl value in settings !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, simplified to settings.LOGIN_URL

from hotosm_auth_django import get_mapped_user_id

if getattr(settings, 'AUTH_PROVIDER', 'legacy') != 'hanko':
return Response({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use exceptions https://github.com/hotosm/fAIr/blob/develop/backend/core/exceptions.py You can have a class for login !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hotosm-auth[django] requires python 3.10 I assume ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 !

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we rename this to hamburger-icon so it's consistent with existing naming convention ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure!

{SHARED_CONTENT.navbar.loginButton}
</Button>
)}
<hotosm-tool-menu></hotosm-tool-menu>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • Must this always exist even in legacy mode ? It doesn't style well in legacy mode.
Image

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

thanks for the detailed feedback!

  1. 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.
  2. disable-active-app attribute: good idea! @warmijusti thoughts?
  3. 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hi! regarding point number 1: thanks for the catch! version 0.2.7 of the tool menu should solve this issue.

hg1g added a commit that referenced this pull request Mar 27, 2026
@kshitijrajsharma kshitijrajsharma merged commit 057e231 into develop Apr 7, 2026
7 checks passed
@kshitijrajsharma kshitijrajsharma deleted the feature/hanko-auth branch April 7, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants