# REANA Tokens Rework Shared Doc ## Introduction As of now, the ESCAPE VRE enables the connection to a REANA cluster via a "[digital bouncer](https://github.com/vre-hub/vre/blob/b5b9defc8644b10ab43df4acce09c1aede023446/containers/iam-reana-sync/add_reana_users.py)" mechanism that maps users from INDIGO IAM to REANA tokens. The aim is to eliminate this middle layer by allowing REANA to directly accept (and use) jwt tokens. This would also streamline the connection of the REANA extension in the VRE to the related REANA cluster, eliminating the need for a login page (or making it optional). Below we attach a tentative flow diagram related to the extension handling of users. ![](https://codimd.web.cern.ch/uploads/upload_4b640f85b3d0237e5a40d91a8a0cc2c4.png) ## Current technology - OAuth is being handled in `reana-server` by `invenio` packages, see [setup.py](https://github.com/reanahub/reana-server/blob/master/setup.py). The docs are available on Invenio side, such as [invenio-oauth2server](https://invenio-oauth2server.readthedocs.io/en/latest/index.html) and [invenio-oauthclient](https://invenio-oauthclient.readthedocs.io/en/latest/index.html) or [InvenioRDM Keycloak](https://inveniordm.docs.cern.ch/customize/authentication/#keycloak). - Pros: Using the same technology stack as other Invenio services (CDS, Zenodo, etc). - Cons: This brings its own database schema, so "invenio tables" and "reana tables" co-exist and eat DB connection pool. And we need to take care when updating Flask/Werkzeug versions due to supported Invenio packages, etc. - OAuth is used only for user authentication. The issued access tokens are custom to REANA, and their lifetime is currently infinite. - There is a wish to automatise token issuance, see https://github.com/reanahub/reana-client/issues/740 - The tokens would ideally have limited lifetime. - In the past we have considered removing local user handling in REANA so that it would be a "single-user system" out of the box, and recommend to plug it into Keycloak/OIDC IAM as the only way of getting multiple users for the instance. (Ditto for user roles and groups.) See for example https://github.com/reanahub/reana/issues/356 - Pros: clean separation, easy to plug REANA to pre-existing university systems. - Pros: no need to support in-application user group handling, can rely on external standards. - Cons: need to radically amend code base and provide upgrade path to (few) existing installations who might be using local users (e.g. PUNCH). - Cons: local Keycloak IAM was not very performant with many user groups (e.g. as is the case at CERN), see some [past benchmarking](https://github.com/DaanRosendal/keycloak-benchmarking) ## Proposals ### Option A: Token exchange on top of current technology stack Keep Invenio in REANA. Add token exchange mechanisms. ### Option B: Revamp completely the token handling. Remove Invenio from REANA. Require Keycloak/OIDC IAM. # Knowledge Base This section serves as a living reference for understanding the current authentication and authorization mechanisms in REANA. It will document the legacy OAuth2 flow via Invenio, the current REANA token issuance and usage, and various service integrations (e.g., ESCAPE IAM, GitLab). ## Obsolete Authentication flow This diagram represents the legacy authentication flow between a client application, REANA server, Invenio OAuth Client, and an Identity Provider (IDP). It highlights the key interactions during user authentication and registration, including: - Initial token exchange - UserInfo endpoint calls for both token verification and user registration - User existence checks and account creation logic ```mermaid sequenceDiagram participant Client participant InvenioOAuth as Invenio OAuth Client inside Reana participant IDP as Identity Provider Client->>InvenioOAuth: Requests Login Note right of InvenioOAuth: Uses ext.py to initialize flow InvenioOAuth->>IDP: Redirect to Login Note right of IDP: Keycloak handles authentication IDP->>InvenioOAuth: Callback with Auth Code Note right of InvenioOAuth: /api/oauth/authorized/keycloak triggered Note right of InvenioOAuth: authorized_handler from handlers/authorized.py InvenioOAuth->>IDP: Exchange Code for Token Note right of InvenioOAuth: Token Verification Phase InvenioOAuth->>IDP: Get UserInfo Note right of InvenioOAuth: Verifies token & gets basic info IDP->>InvenioOAuth: User Info Response alt User Not Found Note right of InvenioOAuth: Registration Flow InvenioOAuth->>IDP: Additional UserInfo (if needed) Note right of InvenioOAuth: May fetch extra attributes IDP->>InvenioOAuth: Complete User Details Note right of InvenioOAuth: Creates new user & links identity else User Exists Note right of InvenioOAuth: Updates tokens only end InvenioOAuth->>Client: Authentication Complete. Stores session ``` # RFC's ## RFC: Authentication Flow Specification ### Goal * Remove all Invenio-related authentication endpoints and packages, including `invenio-oauthclient`, which manages oauth2 redirects and users in the database * REANA server should operate as a standalone service exposing only REANA-specific endpoints. * The server must not handle OAuth2 authentication flows, manage user sessions, or require any OAuth2 clients or redirect handling * Token acquisition should occur entirely outside the REANA server, which will solely validate and authorize tokens issued by the external IDP, without maintaining any user-related state. * JWT authorization is used instead of `invenio-oauthclient` created session ### Problem - Current authentication flow requires REANA backend `/login/keycloack` endpoint even during authentication with ESCAPE IDP - REANA server contains an additional OAuth2 endpoints, duplicating functionality already provided by ESCAPE IDP - REANA Server maintains unnecessary user state during OAuth2 authorization flow and endpoint call through session ids - Flask uses sessions despite future JWT access tokens being session-equivalent themselves - The current session management through cookies adds complexity without providing additional security benefits - Multiple round-trips between client -> server -> IDP increase latency - Using the additional endpoints & its tables (via `invenio-oauthclient` package) increases maintenance burden [//]: # (## Challenges) [//]: # (- Maintaining proper OAuth2 state validation) [//]: # (- Handling callback URLs and redirect URIs) [//]: # (- GitLab integration still needs valid user tokens) [//]: # (- Ensuring secure token handling on client side) [//]: # (- Secure implementation of frontend-only OAuth2 flow) ### Solution #### Direct Frontend OAuth2 Flow The key improvement is moving the OAuth2 flow entirely to the frontend client (browser), eliminating REANA Server from authentication handling: 1. Frontend client directly initiates OAuth2 `authorization_code` flow with the IDP: ```javascript // Current flow with unnecessary server involvement: // Browser -> REANA Server -> IDP -> REANA Server -> Browser // New flow (frontend only): // Browser -> IDP -> Browser const authEndpoint = 'https://invenio-idp/oauth/authorize'; const clientId = 'reana-client'; const redirectUri = 'reana-ui/callback'; const state = generateSecureRandomState(); // PKCE for enhanced security const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const authUrl = new URL(authEndpoint); authUrl.searchParams.append('client_id', clientId); authUrl.searchParams.append('redirect_uri', redirectUri); authUrl.searchParams.append('response_type', 'code'); authUrl.searchParams.append('state', state); authUrl.searchParams.append('code_challenge', codeChallenge); authUrl.searchParams.append('code_challenge_method', 'S256'); ``` 2. Frontend handles OAuth2 callback and token exchange: ```javascript // Frontend directly exchanges code for token with IDP async function handleCallback(code) { const tokenResponse = await fetch('https://invenio-idp/oauth/token', { method: 'POST', body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: clientId, code_verifier: codeVerifier, // PKCE verification redirect_uri: redirectUri }) }); const tokens = await tokenResponse.json(); // Store JWT access token securely, no additional session needed secureStore.set('access_token', tokens.access_token); } ``` 3. Frontend uses it for API Authentication using JWT only: ```javascript // Use JWT directly for API calls, no session management needed fetch('reana-api/workflows', { headers: { 'Authorization': `Bearer ${secureStore.get('access_token')}` } }); ``` Benefits: - Eliminates unnecessary server-side state and flask session management - Reduces latency by removing extra hop through REANA server - Simplifies server architecture and maintenance by removing OAuth2 endpoints - Uses standard `authorization_code` flow with PKCE - Improves security by reducing attack surface - JWT tokens provide stateless authentication without additional session management layer - Unification of access through jwt, avoiding different access patterns like seesions and access passwords Security: - OAuth2 PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks - GitLab supports this [approach](https://docs.gitlab.com/api/oauth2/) - State parameter prevents CSRF attacks - Frontend-only flow reduces attack surface - JWT validation on server side without maintaining sessions - Secure token storage in browser using httpOnly cookies or secure storage ### Current vs Propsed Flow Current Flow (with unnecessary complexity): ```mermaid sequenceDiagram participant Browser participant REANA_Invenio as REANA Invenio<br/>(OAuth2 Endpoints) participant REANA_Server as REANA Server<br/>(API Endpoints) participant ESCAPE_IAM as Keycloak IAM Note over REANA_Invenio,REANA_Server: reana-sever project Browser->>REANA_Invenio: 1. /login/keycloack REANA_Invenio->>ESCAPE_IAM: 2. Redirects user to Keycloak w/ session ESCAPE_IAM-->>REANA_Invenio: 3. Gets token REANA_Invenio-->>Browser: 4. Stores session ID Note over REANA_Invenio,REANA_Server: Token stored in Flask Browser->>REANA_Server: 5. Request + Session ID ``` New Proposed Flow (direct & simplified): ```mermaid sequenceDiagram participant Browser participant ESCAPE_IAM as Keycloak IAM participant REANA_Server as REANA Server Browser->>ESCAPE_IAM: 1. Perform OAuth2.0 flow ESCAPE_IAM-->>Browser: 2. Gets JWT and stores it Browser->>REANA_Server: 3. Request + JWT Note over REANA_Server: Perform auhtorization only on JWT token ``` ## RFC: Supporting Authorization via JWT tokens ### Description We are initiating work to support authorization in `reana-server` using JWT tokens issued by external Identity Providers (IdPs). This RFC focuses exclusively on backend mechanisms for verifying and extracting user claims from tokens in order to enforce authorization decisions. Feedback and suggestions are welcome. --- ### Goals The main goals are: - Enable `reana-server` to accept and validate JWT tokens signed by a trusted Identity Provider. - Extract relevant claims (e.g. user id) from tokens to support authorization logic. - Configure trusted signing keys URL's (e.g. JWKs) and accepted issuer in the REANA deployment. --- ### Use cases **As a researcher,** I would like to authenticate to REANA using a JWT token issued by my organization IdP, so that I can securely access my workflows without creating a new set of credentials. **As a researcher,** I would like REANA to recognize my user identity and roles from the token claims, so that access control can be enforced appropriately. **As a cluster administrator,** I would like to configure a trusted Identity Provider and its public keys URL's, so that REANA can validate incoming tokens securely. --- ### Discussion `reana-server` will support incoming API requests containing a bearer token via the `Authorization` header. It will: - Verify token signature using the configured JWKs or public keys. - Validate standard claims (`sub`) based on configuration. - Extract claims such as `sub` to drive authorization decisions. The logic will be modular to support integration with IdPs by following JWK specification --- ### See also N/A Sure! Here's an RFC in a format and tone consistent with your original, incorporating the new details: --- ## RFC: Mapping JWT IdP Identities to REANA Users ### Description We are initiating work to map users authenticated via JWT tokens from external Identity Providers (IdPs) to internal REANA user records. Since JWT-based authorization relies on stable IdP-issued identifiers (sub) rather than REANA’s internally generated user IDs, this mapping is essential. Currently, these identifiers are not stored in the REANA database. This RFC builds on the JWT authorization RFC and is a prerequisite for claim-based access control. Feedback and suggestions are welcome. --- ### Goals The main goals are: * Persist the `sub` and `iss` (issuer) values from trusted IdPs in REANA's database. * Enable REANA to resolve and match incoming JWT tokens to internal users. * Provide flexibility for deployments supporting a single or multiple IdPs. * Provide a mechanism to bootstrap this mapping through a migration phase. * Lay groundwork for future automatic identity provisioning and propagation (see [Phase 4: User Provisioning and Identity Propagation](https://codimd.web.cern.ch/pb_arxC5RWSZwbXY9HSGIg#Phase-4-User-Provisioning-and-Identity-Propagation)). --- ### Use cases As a researcher, I would like REANA to recognize my identity from the `sub` and `iss` claims in my JWT token, so that I can be correctly authorized to access my resources. As a cluster administrator, I would like to map IdP user identifiers to existing REANA users, so that I can onboard external users without requiring email-based matching. As a cluster administrator, I want the ability to store external IdP identity information in REANA’s database, so that tokens can be resolved efficiently and securely at runtime. --- ### Proposed Design ### Identity Mapping Storage To associate external JWT identities with internal REANA users, we propose two schema design options: --- **Option A: Add a single `idp_id` column to the `User` table** * `idp_id`: A string combining issuer and subject in the format `<iss>:<sub>`, used as a globally unique external identity key. #### Pros: * Minimal schema change (only one new column). * Fast to implement and integrate. * Simple lookup via a single field. #### Cons: * Harder to query/filter by individual parts (`sub`, `iss`). * Less flexible for multi-IdP or multi-account support in the future. * Mixing identity logic with user profile logic. --- **Option B: Add a separate `user_identities` table** ```sql user_identities ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), idp_sub TEXT NOT NULL, idp_iss TEXT NOT NULL, UNIQUE(idp_iss, idp_sub) ) ``` #### Pros: * Clean separation of identity logic from user data. * Supports multiple IdP identities per user (future-proof). * Easier to extend with additional metadata (e.g. linked\_at, identity type). #### Cons: * Slightly more complex schema. * Requires an extra join when resolving user identity. **Option B is preferred** for long-term maintainability and flexibility. --- ### Migration A one-time migration script will be needed to associate existing REANA users with their corresponding external IdP identity: * This process may involve prompting users to authenticate with their IdP and then storing the resolved `sub` and `iss` values. * Alternatively, an administrator-driven batch mapping approach could be used during initial rollout. --- ### Migration Script (Pseudocode) A basic migration strategy to populate user identity mappings, assuming a temporary script that runs outside of authentication and authorization context: ```python # Pseudo-code for a standalone, one-time migration script # Purpose: populate REANA user identity mappings using external IdP data (e.g., SCIM) def fetch_users_from_idp(): """ Fetch user identities from external IdP (e.g., via SCIM or admin API) This should return a list of users with at least 'email', 'sub', and 'iss' """ return scim_api.list_users() # Example placeholder def migrate_identities(): external_users = fetch_users_from_idp() for ext_user in external_users: email = ext_user["email"] sub = ext_user["sub"] iss = ext_user["iss"] # Step 1: Match to internal REANA user via email reana_user = db.users.find_by_email(email) if not reana_user: print(f"⚠️ No matching REANA user for email: {email}") continue # Step 2: Create identity mapping (Option B) db.user_identities.insert({ "user_id": reana_user.id, "idp_sub": sub, "idp_iss": iss }) print(f"✅ Mapped {email} → {iss}:{sub}") # Run the migration (outside of any auth flow) if __name__ == "__main__": migrate_identities() ``` --- ### Token Matching Logic The `reana-server` will extract `sub` and `iss` from incoming JWT tokens and perform a lookup in the user identity mapping table to resolve the internal user ID. This will serve as the basis for authorization logic downstream. --- ### Future Work Automated user provisioning and identity propagation (e.g., user auto-creation on first login, syncing roles/claims) will be addressed in a later phase. *Note: After the current phase, the feature will rely on the presence of migrated IdP identifiers for correct operation.* This is tracked under [Phase 4: User Provisioning and Identity Propagation](https://codimd.web.cern.ch/pb_arxC5RWSZwbXY9HSGIg#Phase-4-User-Provisioning-and-Identity-Propagation). --- # Open Questions * **Frontend Token Storage** How will access tokens be securely stored on the frontend (e.g., `httpOnly` cookies, localStorage, memory)? How often will the token need to be refreshed? * **Frontend OAuth2 Library** What frontend library or tooling is intended for managing the OAuth2.0 flow and token handling? * **REANA Server Involvement** In what scenarios, if any, should the REANA server interact directly with the IDP? (e.g., user info or public key retrieval for JWT verification or any other edge cases) ------------------------------------ * **Client Access Patterns** What are the typical access patterns of REANA clients (e.g., browser UI, CLI, automation scripts)? Are there any existing non-standard flows or workarounds that might not fit the OAuth2.0 model? * **CLI Authentication Flow** Should the CLI support OAuth2-based login via external IDPs? Would using an OAuth2 library on CLI be a viable and maintainable solution? Are we fine with using jwt tokens in cli if they have lower exirations? * **Current GitLab Setup** How should the existing GitLab integration (e.g., access token storage, webhook setup) be handled during or after the transition to the proposed architecture? * **Device Code Flow Support from Indigo** Is it supported? * **Communication Between R1 and R2 (Cross-IAM)** In scenarios where R1 uses **IAM1** and R2 uses **IAM2**, and the same user (U) exists in both identity providers, how should token issuance and validation be handled across services? Should each service independently issue and validate tokens via its own IAM, or should there be a shared trust model or mapping between IAM1 and IAM2? How would token propagation work in a distributed workflow — e.g., can R1-issued tokens be used directly by R2, or would token exchange or re-authentication be required? > The specific details and requirements still need to be defined — particularly regarding authentication initiation, identity federation, and the execution scope of cross-instance requests. # Project Phases This section outlines the planned phases for implementing a new authentication and authorization system in REANA that aligns with existing roadmap. It reflects current internal discussions and establishes a clear sequence based on technical dependencies and user impact. --- ## Phase 1: Introduce Authorization Hooks (Run in Parallel with Invenio) **Goal:** Enable REANA-native access control using JWTs, while maintaining the current Invenio-based setup for compatibility. * Implement JWT-based authorization checks on REANA server endpoints. * Extract user identity solely from the JWT (e.g., the sub claim). * No authentication or user provisioning is handled at this stage. * Run this logic in parallel with the existing Invenio stack to prevent regressions. **Rationale:** This phase enables safe and incremental integration of native authorization without disrupting current users or workflows. --- ## Phase 2: Support REANA JupyterLab Extension Authorization **Goal:** Ensure that REANA’s JupyterLab extension works correctly with the new authorization mechanism. * Modify the extension to use the new access token instead of the legacy token. * Ensure user identity and permissions are enforced based on the token claims. **Rationale:** JupyterLab REANA is a focused extension with a narrower scope, making it ideal for isolated testing. Ensuring early compatibility helps prevent disruptions in broader workflows and integrations. --- ## Phase 3: Client-Side Authentication **Goal:** Design and implement an authentication flow handled entirely on the client side, focusing on the JupyterLab extension while designig solution that fits all integrations. * Introduce a client-driven authentication mechanism that does not require REANA server involvement. * Only the JupyterLab client is affected at this stage. **Rationale:** This approach simplifies the REANA server's role by focusing solely on authorization and leaves authentication to the client. --- ## Phase 4: User Provisioning and Identity Propagation **Goal:** Automatically create REANA user records upon first login using data from an external identity provider (IdP). * Implement Just-in-Time (JIT) provisioning or an equivalent mechanism. * Use token claims (e.g., sub, email) or IdP-provided user info to create REANA users as needed. **Rationale:** User provisioning is a prerequisite for successful user onboarding and depends on the final authentication method defined in Phase 3. --- ## Phase 5: Optional Keycloak Integration **Goal:** Support Keycloak as an optional identity provider for OAuth2 flows. * Implement integration to handle Keycloak-specific claims and configurations. * Keep Keycloak support optional and fully configurable within REANA project. * Use Keycloak to validate end-to-end OAuth integration without relying on Indico IAM. **Rationale:** Keycloak is widely adopted and useful for internal testing and validation of the full OAuth2 stack. --- ## Phase 6: OAuth2 Integration Across UI, CLI, and Services **Goal:** Unify authentication and authorization across all REANA interfaces. * Update the REANA UI to rely entirely on the new OAuth2 token. * Update the REANA CLI to implement the OAuth2 Device Code flow for user login. * Ensure consistent token validation across all REANA services. * Monitor and phase out any remaining reliance on the legacy system. **Rationale:** Unified token-based flows across all interfaces increase consistency, security, and maintainability. --- ## Phase 7: Comprehensive Testing **Goal:** Thoroughly validate the new OAuth2-based authentication and authorization system. * Test all interfaces: CLI, UI, JupyterLab. * Include edge cases such as token expiration, reuse, invalid or missing claims, and provisioning failures. * Ensure the system functions with token-based identity only, without fallback to Invenio. **Rationale:** Robust testing ensures a safe deprecation of the legacy stack and builds confidence in the new system. --- ## Phase 8: Communication and Rollout **Goal:** Effectively inform users and deployment engineers of the new authentication system. * Communicate changes and RFCs via GitHub and mailing lists. * Provide clear documentation and migration guides for users and operators. * Offer guidance on how to test and adopt the new system. **Rationale:** Clear communication eases the transition and minimizes user disruption. --- ## Phase 9: Deprecate and Remove Invenio Stack **Goal:** Fully retire all legacy authentication components based on Invenio. * Remove related services, configuration, and dependencies. * Validate that REANA operates fully under the new OAuth2-based system. **Rationale:** Final cleanup simplifies the architecture and confirms a successful migration. # Demo and Development Apply changes in sequence across repos: `reana-server` → `reana-commons` → `reana-client`. Each PR/component is self-contained and uses only its own config files (e.g., in `reana-server`); all config values must be set. `reana-client`, `reana-server`, and `reana-commons` each have a `demo` branch with changes merged to help run the full stack. > **Recommendation:** To keep the cascade lighter, implement work strictly in that order across repos. Validate each repo on its own without relying on other services. Prefer automated, reproducible unit tests for pure logic and small, targeted integration/contract tests—so you isolate behavior and avoid complex full-stack integrations, which are not reproducible, time-consuming, and don’t isolate components well. ### Known pitfalls we hit in a full demo with all components & how we addressed them 1. **Self-signed certificates inside the cluster** - **Symptom:** TLS verification failures on intra-cluster HTTP calls. - **Workaround used:** add `verify=False` to Python HTTP calls in **`reana-client`** and **`reana-server`**. 2. **IdP client configuration (client-id)** - **Keycloak** deployment from scratch required these adjustments. - The IdP client **must**: - Support and have **Device Code Flow** enabled, - **Pass `sub`** identity in JWT tokens, - Be configured as a **public client**. - Create the client **beforehand** and place its **client id** into **`reana-server`** via config. - **Shortcut:** reuse my **ESCAPE** client to skip full IdP setup (`reana-oauth2-implementation` client name and `f671a136-8e92-45e5-83bd-05af1942e396` client id) 3. **Cross-repo dependency ordering** - Sequential dependencies: **`reana-client` → `reana-commons` → `reana-server`**. - **Action:** after pulling the **`demo`** branch, ensure **`reana-commons`** changes are **merged** and **installed locally** into **`reana-client`**.
{}