|
| 1 | +%% Validated against OpenHands-Cloud @ 136f63fdc |
| 2 | + |
| 3 | +sequenceDiagram |
| 4 | + autonumber |
| 5 | + participant Browser |
| 6 | + participant OpenHands as OpenHands<br/>(Enterprise Server) |
| 7 | + participant DB as SecretsStore |
| 8 | + participant Keycloak as Keycloak<br/>(Identity Provider / OAuth Broker) |
| 9 | + participant GitHub as GitHub<br/>(GitHub App, OAuth 2.0) |
| 10 | + |
| 11 | + Note over Browser,GitHub: This deployment uses a GitHub APP (not a classic OAuth App).<br/>Evidence: GitHub App client_id has the Iv__ prefix and<br/>the returned user token is prefixed ghu_ (App user-to-server).<br/>GitHub Apps grant capabilities via App-level permissions,<br/>so OAuth scope strings are largely ignored.<br/>User tokens expire (8 h) and the App returns a refresh token<br/>that OpenHands rotates via grant_type=refresh_token. |
| 12 | + |
| 13 | + Note over Browser,Keycloak: OIDC. SPA initiates login client-side. |
| 14 | + Note over Browser: SPA at /login builds URL via<br/>generate-auth-url.ts and assigns<br/>window.location.href |
| 15 | + |
| 16 | + Browser->>Keycloak: GET /realms/allhands/protocol/openid-connect/auth<br/>?client_id=allhands<br/>&response_type=code<br/>&kc_idp_hint=github<br/>&redirect_uri=.../oauth/keycloak/callback<br/>&scope=openid+email+profile<br/>&state=base64(redirect_url) |
| 17 | + |
| 18 | + Note over Keycloak: kc_idp_hint=github skips Keycloak login screen.<br/>Keycloak does NOT forward the SPA OIDC scopes.<br/>It sends its own configured defaultScope to GitHub. |
| 19 | + |
| 20 | + Keycloak-->>Browser: 303 See Other<br/>/realms/allhands/broker/github/login?session_code=... |
| 21 | + |
| 22 | + Browser->>Keycloak: GET /realms/allhands/broker/github/login |
| 23 | + Keycloak-->>Browser: 303 See Other to github.com |
| 24 | + |
| 25 | + Note over Browser,GitHub: OAuth 2.0 (NOT OIDC). Keycloak<br/>built-in providerId=github broker. |
| 26 | + Browser->>GitHub: GET /login/oauth/authorize<br/>?client_id=GITHUB_APP_CLIENT_ID -- Iv__ prefix marks GitHub App<br/>&redirect_uri=.../realms/allhands/broker/github/endpoint<br/>&response_type=code<br/>&scope=openid+email+profile -- OIDC strings, not GitHub OAuth scopes<br/>&state=keycloak-state |
| 27 | + |
| 28 | + Note over GitHub: Scope strings openid/email/profile are<br/>NOT GitHub native scopes (user, user:email, ...).<br/>GitHub Apps ignore them. App permissions are<br/>configured on github.com and apply uniformly.<br/>User authorizes the App (first time) or is auto-redirected. |
| 29 | + |
| 30 | + GitHub-->>Browser: 302 to Keycloak broker endpoint |
| 31 | + Browser->>Keycloak: GET /realms/allhands/broker/github/endpoint<br/>?code=github-auth-code<br/>&state=keycloak-state |
| 32 | + |
| 33 | + Note over Keycloak,GitHub: Server-to-server (no browser). |
| 34 | + Keycloak->>GitHub: POST /login/oauth/access_token<br/>client_id, client_secret, code, redirect_uri |
| 35 | + GitHub-->>Keycloak: access_token=ghu_...<br/>refresh_token=ghr_...<br/>expires_in, refresh_token_expires_in |
| 36 | + |
| 37 | + Keycloak->>GitHub: GET https://api.github.com/user<br/>Authorization: Bearer ghu_... |
| 38 | + GitHub-->>Keycloak: id, login, email, ... |
| 39 | + |
| 40 | + Note over Keycloak: Maps github id to user attribute github_id<br/>via github-user-attribute-mapper.<br/>Hardcodes identity_provider=github.<br/>Persists tokens because storeToken=true. |
| 41 | + |
| 42 | + Keycloak-->>Browser: 302 to OpenHands |
| 43 | + Browser->>OpenHands: GET /oauth/keycloak/callback<br/>?code=kc-code.session.client<br/>&session_state=...&iss=... |
| 44 | + |
| 45 | + Note over OpenHands,Keycloak: Server-to-server OIDC. |
| 46 | + OpenHands->>Keycloak: POST /realms/allhands/protocol/openid-connect/token<br/>grant_type=authorization_code, code,<br/>client_id=allhands, client_secret |
| 47 | + Keycloak-->>OpenHands: access_token (JWT), id_token (JWT), refresh_token<br/>NO offline_access on this leg |
| 48 | + |
| 49 | + OpenHands->>Keycloak: GET /realms/allhands/protocol/openid-connect/userinfo<br/>Authorization: Bearer kc_access_token |
| 50 | + Keycloak-->>OpenHands: sub, email, preferred_username, github_id, identity_provider |
| 51 | + |
| 52 | + Note over OpenHands: Logs (verbatim):<br/>Logging in user UUID in org UUID<br/>user_logged_in idp=github idp_type=oidc |
| 53 | + |
| 54 | + Note over OpenHands,Keycloak: Fetch broker-stored GitHub token. |
| 55 | + OpenHands->>Keycloak: GET /realms/allhands/broker/github/token<br/>Authorization: Bearer kc_access_token |
| 56 | + Keycloak-->>OpenHands: access_token=ghu_...<br/>refresh_token=ghr_...<br/>expires_at, refresh_expires_at |
| 57 | + OpenHands->>DB: Encrypt and persist tokens (AuthTokenStore) |
| 58 | + |
| 59 | + Note over OpenHands: server/routes/auth.py branches on two<br/>independent flags - valid_offline_token and<br/>has_accepted_tos. First login fails both.<br/>Returning users typically pass both. |
| 60 | + |
| 61 | + alt has_accepted_tos == false (first login) |
| 62 | + OpenHands-->>Browser: 302 to /accept-tos<br/>?redirect_url=(offline_auth_url_or_app) |
| 63 | + Note over Browser: SPA renders TOS. User clicks accept. |
| 64 | + Browser->>OpenHands: POST /api/accept_tos |
| 65 | + OpenHands-->>Browser: 200 OK, body has redirect_url<br/>(NOT a 302) |
| 66 | + Note over Browser: SPA reads redirect_url and assigns<br/>window.location.href = redirect_url |
| 67 | + end |
| 68 | + |
| 69 | + alt valid_offline_token == false (first login, or offline session expired) |
| 70 | + Note over Browser,Keycloak: Second OIDC flow. Captures offline (refresh) token.<br/>Keycloak SSO session is live, so no GitHub round-trip. |
| 71 | + Browser->>Keycloak: GET /realms/allhands/protocol/openid-connect/auth<br/>scope=openid+email+profile+offline_access<br/>redirect_uri=.../oauth/keycloak/offline/callback |
| 72 | + Keycloak-->>Browser: 302 with new authorization code |
| 73 | + Browser->>OpenHands: GET /oauth/keycloak/offline/callback?code=... |
| 74 | + OpenHands->>Keycloak: POST /protocol/openid-connect/token |
| 75 | + Keycloak-->>OpenHands: access_token + LONG-LIVED refresh_token (offline) |
| 76 | + end |
| 77 | + |
| 78 | + OpenHands-->>Browser: Set session cookies, redirect to app<br/>(returning users land here directly with no TOS/offline detour) |
| 79 | + |
| 80 | + Note over Browser,GitHub: Subsequent calls. GitHub token from local store. |
| 81 | + Browser->>OpenHands: GET /api/v1/git/installations/search?provider=github |
| 82 | + OpenHands->>DB: load_tokens(github) |
| 83 | + |
| 84 | + alt access_token near expiry (less than 15 min left) and refresh_token still valid |
| 85 | + Note over OpenHands,GitHub: token_manager._refresh_github_token<br/>(gated by AuthTokenStore._is_token_expired<br/>with ACCESS_TOKEN_EXPIRY_BUFFER = 900s) |
| 86 | + OpenHands->>GitHub: POST https://github.com/login/oauth/access_token<br/>client_id, client_secret, refresh_token,<br/>grant_type=refresh_token |
| 87 | + GitHub-->>OpenHands: new access_token + new refresh_token<br/>expires_in, refresh_token_expires_in |
| 88 | + OpenHands->>DB: re-encrypt and persist |
| 89 | + else access_token still valid |
| 90 | + Note over OpenHands: Use stored ghu_... as-is |
| 91 | + end |
| 92 | + |
| 93 | + OpenHands->>GitHub: GET https://api.github.com/...<br/>Authorization: Bearer ghu_... |
| 94 | + GitHub-->>OpenHands: Repos / installations / etc. |
| 95 | + OpenHands-->>Browser: JSON response |
0 commit comments