-
-
Notifications
You must be signed in to change notification settings - Fork 79
Add Hanko SSO Authentication #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
540c48b
acd00d0
8e07af0
da3cb38
e462ee7
ec3cf59
3021668
ab126b1
afc6976
b0d2500
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # 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 | ||
|
|
||
| ## 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
|
|
||
| from core.views import home | ||
| from django.conf import settings | ||
| from fairproject.settings import AuthProvider | ||
| from django.conf.urls import include | ||
| from django.contrib import admin | ||
| from django.urls import path | ||
|
|
@@ -25,14 +26,26 @@ | |
| SpectacularSwaggerView, | ||
| ) | ||
|
|
||
| admin_mapping_patterns = [] | ||
| if settings.AUTH_PROVIDER == AuthProvider.HANKO: | ||
| from hotosm_auth_django.admin_routes import create_admin_urlpatterns | ||
| admin_mapping_patterns = create_admin_urlpatterns( | ||
| app_name="fair", | ||
| user_model="login.OsmUser", | ||
| user_id_column="osm_id", | ||
| user_name_column="username", | ||
| user_email_column="email", | ||
| ) | ||
|
|
||
| urlpatterns = [ | ||
| path("api/schema/", SpectacularAPIView.as_view(), name="schema"), | ||
| path("api/swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), | ||
| path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), | ||
| path("api/", home, name="home"), | ||
| 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)), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why ? what is api admin mapping patterns ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| path("django-admin/", admin.site.urls), | ||
| ] | ||
|
|
||
| if settings.DEBUG: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,22 @@ | ||
| import logging | ||
| from django.conf import settings | ||
| from osm_login_python.core import Auth | ||
| from fairproject.settings import AuthProvider | ||
| from rest_framework import authentication, exceptions | ||
|
|
||
| from .models import OsmUser | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class OsmAuthentication(authentication.BaseAuthentication): | ||
| class LegacyOsmAuthentication(authentication.BaseAuthentication): | ||
| """Legacy OSM OAuth authentication using osm_login_python. | ||
|
|
||
| Used when AUTH_PROVIDER="legacy". | ||
| Reads access-token from header and validates directly with OSM. | ||
| """ | ||
| def authenticate(self, request): | ||
| from osm_login_python.core import Auth | ||
|
|
||
| access_token = request.headers.get( | ||
| "access-token" | ||
| ) # get the access token as header | ||
|
|
@@ -56,3 +63,46 @@ def authenticate(self, request): | |
| "OSM authentication failed: Invalid or expired access token" | ||
| ) | ||
| return (user, None) # authentication successful return id,user_name,img | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to remove the inline comments ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. restored! |
||
|
|
||
|
|
||
| class HankoAuthentication(authentication.BaseAuthentication): | ||
| """Hanko SSO authentication using user mappings.""" | ||
| def authenticate(self, request): | ||
| from hotosm_auth_django import get_mapped_user_id | ||
|
|
||
| if not hasattr(request, 'hotosm'): | ||
| raise exceptions.AuthenticationFailed( | ||
| "HankoAuthMiddleware not configured" | ||
| ) | ||
|
|
||
| hanko_user = request.hotosm.user | ||
|
|
||
| if not hanko_user: | ||
| logger.debug("No Hanko user in request") | ||
| return (None, None) | ||
|
|
||
| mapped_osm_id = get_mapped_user_id(hanko_user, app_name="fair") | ||
|
|
||
| if mapped_osm_id is not None: | ||
| try: | ||
| osm_id = int(mapped_osm_id) | ||
| user = OsmUser.objects.get(osm_id=osm_id) | ||
| logger.debug(f"Authenticated via mapping: Hanko={hanko_user.email}, osm_id={osm_id}") | ||
| return (user, None) | ||
| except (OsmUser.DoesNotExist, ValueError) as e: | ||
| logger.warning(f"Mapping exists but user not found: osm_id={mapped_osm_id}, error={e}") | ||
| # Fall through to onboarding. | ||
|
|
||
| request.needs_onboarding = True | ||
| request.hanko_user_for_onboarding = hanko_user | ||
| logger.debug(f"Hanko user {hanko_user.email} needs onboarding (no mapping)") | ||
| return (None, None) | ||
|
|
||
|
|
||
| # Select authentication class based on AUTH_PROVIDER | ||
| if settings.AUTH_PROVIDER == AuthProvider.HANKO: | ||
| logger.info("Using Hanko SSO authentication") | ||
| OsmAuthentication = HankoAuthentication | ||
| else: | ||
| logger.info("Using legacy OSM authentication") | ||
| OsmAuthentication = LegacyOsmAuthentication | ||
There was a problem hiding this comment.
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 !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!