Ë
    âß²j'D  ã                  ó^  — U d Z ddlmZ ddlZddlZddlZddlZddlZddl	m
Z
mZmZ ddlZddlmZmZmZ ddlmZmZmZmZmZmZmZmZmZmZmZmZm Z m!Z!  ejD                  e#«      Z$dZ%dZ&d	Z'd
Z(dZ)da*de+d<   dd„Z,dd„Z- G d„ de«      Z.dd„Z/dd„Z0dd„Z1ddl2Z2ddl3Z3ddl4Z4dddddœZ5d„ Z6y) u‡  SelfHostedOIDCProvider â€” generic self-hosted OpenID Connect dashboard auth.

A plain OIDC Relying Party (Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta, â€¦): discovers
endpoints from ``{issuer}/.well-known/openid-configuration``, builds the PKCE (S256) authorize
URL, exchanges the code, and verifies the **ID token** (the access token is opaque per spec)
against the discovered ``jwks_uri`` with ``iss``/``aud`` pinned. Public and confidential
(``client_secret`` layered on top of PKCE, never replacing it) clients both work. Config:
``dashboard.oauth.self_hosted.{issuer,client_id,scopes,client_secret}`` or ``HERMES_DASHBOARD_OIDC_*``.
é    )ÚannotationsN)ÚAnyÚDictÚOptional)Ú
LoginStartÚProviderErrorÚSession)ÚJSON_HEADERSÚTOKEN_ENDPOINT_TIMEOUT_SECÚJwtOAuthProviderÚSkipRegistrationÚexchange_tokenÚload_config_sectionÚparse_json_bodyÚpkce_login_startÚrefresh_token_fromÚregister_providerÚresolve_env_or_cfgÚsession_from_claimsÚvalidate_redirect_uriÚ
verify_jwtzdashboard-auth-self-hostedzopenid profile email)ÚRS256ÚES256ÚRS384ÚRS512ÚES384ÚES512g      $@i  Ú ÚstrÚLAST_SKIP_REASONc               óÆ   — t         j                  j                  | «      }|j                  dk(  s!|j                  dk(  r|j                  xs ddv r| S t        d|› d| ›�«      ‚)z‰Reject non-HTTPS endpoint URLs (loopback http allowed) so a misconfigured
    issuer can't ship auth codes / refresh tokens in cleartext.ÚhttpsÚhttpr   )Ú	localhostz	127.0.0.1z::1zOIDC z. must be https:// (or http on localhost), got )ÚurllibÚparseÚurlparseÚschemeÚhostnamer   )ÚurlÚfieldÚparseds      úP/home/ubuntu/.hermes/hermes-agent/plugins/dashboard_auth/self_hosted/__init__.pyÚ_require_https_or_loopbackr.   :   sa   € ô �\‰\×"Ñ" 3Ó'€FØ‡}�}˜Ò F§M¡M°VÒ$;ÀÇÁÒAVÐTVÐ[|Ñ@|Øˆ
Ü
˜% ˜wÐ&TÐUXÐT[Ð\Ó
]Ð]ó    c                óö   — t         j                  j                  | «      }|j                  j	                  «       }||j
                  xs dj	                  «       |j                  xs dddœj                  |«      fS )z‘``(scheme, hostname, port)`` for an origin compare, with default ports
    normalised so ``https://h`` and ``https://h:443`` are the same origin.r   i»  éP   )r"   r#   )r%   r&   r'   r(   Úlowerr)   ÚportÚget)r*   Úpartsr(   s      r-   Ú_originr6   C   si   € ô �L‰L×!Ñ! #Ó&€EØ�\‰\×ÑÓ!€FØ�U—^‘^Ò) r×0Ñ0Ó2Ø�J‰JÒ@ C°Ñ4×8Ñ8¸Ó@ðBð Br/   c                  óž   — e Zd ZdZdZdZeddœdd„Zdd„Zdd„Z	dd	„Z
dd
„Zdddœ	 	 	 	 	 	 	 	 	 dd„Zdd„Zdd„Zdd„Zdd„Zdd„ZeZdd„Zy)ÚSelfHostedOIDCProviderzHGeneric self-hosted OpenID Connect provider (authorization-code + PKCE).zself-hostedzSelf-Hosted OIDCr   )ÚscopesÚclient_secretc               óh  — |st        d«      ‚|st        d«      ‚|j                  d«      | _        t        | j                  d¬«       || _        |j                  «       xs t        | _        |xs dj                  «       | _        d | _	        d| _
        t        j                  «       | _        d | _        y )Nzissuer is requiredzclient_id is requiredú/Úissuer©r+   r   g        )Ú
ValueErrorÚrstripÚ_issuerr.   Ú
_client_idÚstripÚ_DEFAULT_SCOPESÚ_scopesÚ_client_secretÚ
_discoveryÚ_discovery_fetched_atÚ	threadingÚLockÚ_discovery_lockÚ_jwks_client)Úselfr=   Ú	client_idr9   r:   s        r-   Ú__init__zSelfHostedOIDCProvider.__init__R   s™   € ÙÜÐ1Ó2Ð2ÙÜÐ4Ó5Ð5ð —}‘} SÓ)ˆŒÜ" 4§<¡<°xÕ@Ø#ˆŒØ—|‘|“~Ò8¬ˆŒð  -Ò2°×9Ñ9Ó;ˆÔð 26ˆŒØ,/ˆÔ"Ü(Ÿ~™~Ó/ˆÔØ!%ˆÕr/   c               ó„   — t        |«       | j                  «       }t        |d   | j                  | j                  |¬«      S )NÚauthorization_endpoint)rN   ÚscopeÚredirect_uri)r   Ú_get_discoveryr   rB   rE   )rM   rS   Údiscos      r-   Ústart_loginz"SelfHostedOIDCProvider.start_loging   s?   € ä˜lÔ+Ø×#Ñ#Ó%ˆÜØÐ*Ñ+°t·±ÈdÏlÉlÐiuôwð 	wr/   c               ó˜  — |sy 	 | j                  «       }t        |j                  d«      xs d«      j	                  «       }|sy | j                  |«      \  }}|d| j                  dœ|¥}	 t        j                  ||i t        ¥|¥t        ¬«       y # t        $ r Y y w xY w# t        $ r }t        j                  d|«       Y d }~y d }~ww xY w)NÚrevocation_endpointr   Úrefresh_token)ÚtokenÚtoken_type_hintrN   )ÚdataÚheadersÚtimeoutz-self-hosted OIDC: revoke failed (ignored): %s)rT   r   r   r4   rC   Ú_token_endpoint_authrB   ÚhttpxÚpostr
   Ú_TOKEN_ENDPOINT_TIMEOUT_SECÚ	ExceptionÚloggerÚdebug)rM   rY   rU   ÚendpointÚ
extra_dataÚextra_headersr\   Úexcs           r-   Úrevoke_sessionz%SelfHostedOIDCProvider.revoke_sessionn   sÚ   € ñ Øð	Ø×'Ñ'Ó)ˆEô �u—y‘yÐ!6Ó7Ò=¸2Ó>×DÑDÓFˆÙØà$(×$=Ñ$=¸eÓ$DÑ!ˆ
�MØ&¸?ÐY]×YhÑYhÑwÐlvÐwˆð	OÜ�J‰J�x dÐ4U´|Ð4UÀ}Ð4UÔ_zÕ{ð øô ò 	Ùð	ûô ò 	OÜ�L‰LÐHÈ#×NÐNØûð	Oús)   …B Á+%B  Â	BÂBÂ 	C	Â)CÃC	c                ó¢  — | j                   si i fS |j                  d«      xs g }d|v rd|vrd| j                   ii fS t        j                  j	                  | j
                  d¬«      › dt        j                  j	                  | j                   d¬«      › �}i dd	t        j                  |j                  d
«      «      j                  d«      › �ifS )uS  ``(extra_data, extra_headers)`` for token-endpoint client auth. Public client â†’
        ``({}, {})`` (PKCE alone). Confidential client â†’ ``client_secret_post`` when the IDP
        advertises it *without* ``client_secret_basic``, else HTTP Basic (the OIDC default
        and the fallback when nothing is advertised). RFC 6749 Â§2.3.1.Ú%token_endpoint_auth_methods_supportedÚclient_secret_postÚclient_secret_basicr:   r   )Úsafeú:ÚAuthorizationzBasic zutf-8Úascii)
rF   r4   r%   r&   ÚquoterB   Úbase64Ú	b64encodeÚencodeÚdecode)rM   rU   ÚmethodsÚuserpasss       r-   r_   z+SelfHostedOIDCProvider._token_endpoint_auth…   sÐ   € ð
 ×"Ò"Ø�r�6ˆMØ—)‘)ÐCÓDÒJÈˆØ 7Ñ*Ð/DÈGÑ/SØ# T×%8Ñ%8Ð9¸2Ð=Ð=ô —l‘l×(Ñ(¨¯©¸rÐ(ÓBÐCÀ1ÄVÇ\Á\×EWÑEWÐX\×XkÑXkÐrtÐEWÓEuÐDvÐwˆØ�O v¬f×.>Ñ.>¸x¿¹ÈwÓ?WÓ.X×._Ñ._Ð`gÓ.hÐ-iÐ%jÐkÐkÐkr/   c                ó<   — d| j                   || j                  dœd fS )NrY   )Ú
grant_typerN   rY   rR   )rB   rE   )rM   rY   s     r-   Ú_refresh_requestz'SelfHostedOIDCProvider._refresh_request”   s)   € ð +¸¿¹Ð[hØ—l‘lñ$àðð 	r/   N)r]   Úprevious_refresh_tokenc          
     óà   — | j                  «       }| j                  |«      \  }}t        |d   i |¥|¥||dddd¬«      \  }}	| j                  |«      }
| j	                  |t        |	|«      |
«      S )uï   POST the discovered token endpoint and turn the response into a Session.
        Confidential-client auth (body field or Basic header) is added for both grants â€”
        the IDP rejects an unauthenticated refresh with ``invalid_client``.Útoken_endpointÚIDPzOIDC token endpointÚid_tokenu‚   OIDC token response missing id_token â€” ensure the 'openid' scope is configured and the client is allowed to receive an ID token.)r]   Úbad_request_excÚidprf   Ú	token_keyÚmissing_msg)rT   r_   r   Ú_verify_id_tokenÚ_sessionr   )rM   r\   r‚   r]   r}   rU   rg   rh   r�   ÚpayloadÚclaimss              r-   Ú_grantzSelfHostedOIDCProvider._grantœ   s�   € ð ×#Ñ#Ó%ˆØ$(×$=Ñ$=¸eÓ$DÑ!ˆ
�MÜ*ØÐ"Ñ#Ð%;¨Ð%;°
Ð%;À]ÐdsØÐ 5Àðô	Ñˆ�'ð ×&Ñ& xÓ0ˆà�}‰}˜XÔ'9¸'ÐCYÓ'ZÐ\bÓcÐcr/   c                ó„   — | j                   �4t        j                  «       | j                  z
  t        k  r| j                   S y ©N)rG   ÚtimerH   Ú_DISCOVERY_CACHE_TTL_SEC©rM   s    r-   Ú_fresh_discoveryz'SelfHostedOIDCProvider._fresh_discovery²   s2   € Ø�?‰?Ð&¬4¯9©9«;¸×9SÑ9SÑ+SÔVnÒ+nØ—?‘?Ð"Ør/   c                ó  — | j                  «       }|€`| j                  5  | j                  «       }|€7| j                  «       x}| _        t	        j                  «       | _        d| _        ddd«       |S |S # 1 sw Y   |S xY w)zSReturn the cached OIDC discovery document, fetching if stale (double-checked lock).N)r�   rK   Ú_fetch_discoveryrG   r�   rH   rL   )rM   rU   s     r-   rT   z%SelfHostedOIDCProvider._get_discovery·   s~   € à×%Ñ%Ó'ˆØˆ=Ø×%Ñ%ñ -Ø×-Ñ-Ó/�Ø�=Ø.2×.CÑ.CÓ.EÐE�E˜DœOÜ15·±³�DÔ.Ø(,�DÔ%÷-ð ˆˆuˆ÷-ð ˆús   ŸA
A5Á5A?c                ó  ‡— | j                   › d�}	 t        j                  |t        t        d¬«      }|j                  dk7  rt        d|j                  › d|›�«      ‚t        t        |j                  «      «      t        | j                   «      k7  r&t        d|j                  › d	| j                   ›d
�«      ‚t        |«      Š‰st        d«      ‚dˆfd„}dD �ci c]  }| ||«      “Œ }}t        |j                  «       «      st        d«      ‚ |d«      }|r9|j                  d«      | j                   k7  rt        d|›d| j                   ›�«      ‚|j                  «       D ]  \  }}t!        ||¬«       Œ ‰j                  d«      }	d|xs | j                   i|¥ |d«      t#        |	t$        «      r|	D �
cg c]  }
t        |
«      ‘Œ c}
dœ¥S g dœ¥S # t        j
                  $ r}t        d|› �«      |‚d }~ww xY wc c}w c c}
w )Nz!/.well-known/openid-configurationT)r]   r^   Úfollow_redirectszOIDC discovery unreachable: éÈ   zOIDC discovery returned z for zOIDC discovery resolved to z*, outside the configured issuer's origin (ú)z'OIDC discovery returned a non-JSON bodyc                ó^   •— t        ‰j                  | d«      xs d«      j                  «       S )Nr   )r   r4   rC   )Úkeyrˆ   s    €r-   r+   z6SelfHostedOIDCProvider._fetch_discovery.<locals>.fieldÝ   s'   ø€ Ü�w—{‘{ 3¨Ó+Ò1¨rÓ2×8Ñ8Ó:Ð:r/   )rQ   r   Újwks_urizPOIDC discovery missing one of authorization_endpoint / token_endpoint / jwks_urir=   r<   z4OIDC discovery issuer mismatch: document advertises z but configured issuer is r>   rl   rX   )rX   rl   )r˜   r   Úreturnr   )rA   r`   r4   r
   Ú_DISCOVERY_TIMEOUT_SECÚRequestErrorr   Ústatus_coder6   r   r*   r   ÚallÚvaluesr@   Úitemsr.   Ú
isinstanceÚlist)rM   r*   Úresponseri   r+   ÚkÚ	endpointsÚadvertised_issuerr˜   Úauth_methods_rawÚmrˆ   s              @r-   r’   z'SelfHostedOIDCProvider._fetch_discoveryÃ   s8  ø€ Ø—‘�Ð?Ð@ˆð	Oô
 —y‘y ¬lÔDZÐmqÔrˆHð ×Ñ 3Ò&ÜÐ":¸8×;OÑ;OÐ:PÐPUÐVYÐU\Ð ]Ó^Ð^ô ”3�x—|‘|Ó$Ó%¬°·±Ó)>Ò>ÜØ-¨h¯l©l¨^ð <$Ø$(§L¡LÐ#3°1ð6ó7ð 7ô " (Ó+ˆÙÜÐ IÓJÐJõ	;ð +cÖc Q�Q™˜a›‘[Ðcˆ	ÐcÜ�9×#Ñ#Ó%Ô&ÜÐ rÓsÐsñ " (›OÐÙÐ!2×!9Ñ!9¸#Ó!>À$Ç,Á,Ò!NÜØFÐGXÐF[ð \,Ø,0¯L©LÐ+;ð=ó>ð >ð "Ÿ™Ó)ò 	7‰HˆC�Ü& s°#Ö6ð	7ð #Ÿ;™;Ð'NÓOÐàÐ'Ò7¨4¯<©<ðcàðcñ $)Ð)>Ó#?ä6@ÐAQÔSWÔ6XÐ!1Ö2˜A”�Q•Ò2òcð 	cð
 _aòcð 	cøôG ×!Ñ!ò 	OÜÐ">¸s¸eÐ DÓEÈ3ÐNûð	Oüò* dùò& 3s#   ’!G ÃG=Æ6HÇG:Ç&G5Ç5G:c                ó(   — | j                  «       d   S )Nr™   )rT   r�   s    r-   Ú	_jwks_uriz SelfHostedOIDCProvider._jwks_uri÷   s   € Ø×"Ñ"Ó$ ZÑ0Ð0r/   c                ó”   — | j                  «       d   }t        || j                  «       t        t        «      | j
                  |d¬«      S )Nr=   úID token)Ú
algorithmsÚaudiencer=   Úlabel)rT   r   Ú_get_jwks_clientr¢   Ú_ALLOWED_ID_TOKEN_ALGSrB   )rM   r�   r=   s      r-   r†   z'SelfHostedOIDCProvider._verify_id_tokenú   sE   € Ø×$Ñ$Ó& xÑ0ˆÜØ�d×+Ñ+Ó-¼$Ô?UÓ:VØ—_‘_¨V¸:ôGð 	Gr/   c                óê  — t        |j                  dd«      xs d«      }|j                  d«      xs |j                  d«      xs d}|j                  d«      }|s*t        |t        «      r|rdj	                  d„ |D «       «      }t        | j                  |||d|t        |j                  d	«      xs, |j                  d
«      xs |j                  d«      xs |xs d«      t        |xs d«      ¬«      S )u  Map verified OIDC claims onto a Session. The verified ID token is stored in
        ``Session.access_token`` so the per-request ``verify_session`` re-verifies a real
        JWT; the opaque OAuth access token is not kept â€” the dashboard only needs identity.Úemailr   Úorg_idÚorganizationÚgroupsú,c              3  ó2   K  — | ]  }t        |«      –— Œ y ­wrŒ   )r   )Ú.0Úgs     r-   ú	<genexpr>z2SelfHostedOIDCProvider._session.<locals>.<genexpr>  s   è ø€ Ò5¨œc !ŸfÑ5ùs   ‚r¬   ÚnameÚpreferred_usernameÚnickname)Úaccess_tokenrY   r¯   r³   Údisplay_namer´   )r   r4   r¡   r¢   Újoinr   r¼   )rM   r�   rY   r‰   r³   r´   r¶   s          r-   r‡   zSelfHostedOIDCProvider._session  sÛ   € ô �F—J‘J˜w¨Ó+Ò1¨rÓ2ˆð —‘˜HÓ%ÒI¨¯©°NÓ)CÒIÀrˆØ—‘˜HÓ%ˆÙœ* V¬TÔ2±vØ—X‘XÑ5¨fÔ5Ó5ˆFÜ"Ø�I‰I�v¨HÀMÐYcÐkpÜ˜VŸZ™Z¨Ó/Ò|°6·:±:Ð>RÓ3SÒ|ÐW]×WaÑWaÐblÓWmÒ|ÐqvÒ|Ðz|Ó}Ü�v’| Ó$ô&ð 	&r/   )
r=   r   rN   r   r9   r   r:   r   rš   ÚNone)rS   r   rš   r   )rY   r   rš   rÂ   )rU   úDict[str, Any]rš   z%tuple[Dict[str, str], Dict[str, str]])rY   r   rš   z/tuple[Dict[str, str], Optional[Dict[str, str]]])
r\   zDict[str, str]r‚   ztype[Exception]r]   zOptional[Dict[str, str]]r}   r   rš   r	   )rš   zDict[str, Any] | None)rš   rÃ   )rš   r   )r�   r   rš   rÃ   )r�   r   rY   r   r‰   rÃ   rš   r	   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r¼   rÀ   rD   rO   rV   rj   r_   r|   rŠ   r�   rT   r’   rª   r†   Ú_claims_forr‡   © r/   r-   r8   r8   L   s—   „ ÙRà€DØ%€LàETÐkmõ &ó*wóó.lóð nrØ&(ñdØ"ðdØ8GðdØRjðdà #ðdð 
ódó,ó

ó0cóh1óGð #€Kô&r/   r8   c                 ó0   — t        t        t        ddd«      S )NÚ	dashboardÚoauthÚself_hosted)r   rd   Ú_TAGrÉ   r/   r-   Ú_load_config_oauth_sectionrÏ     s   € Üœv¤t¨[¸'À=ÓQÐQr/   c                 óÒ   ‡— t        «       Šdˆfd„}  | dd«      } | dd«      }|r|s$t        dt        |«      ›dt        |«      ›d�«      ‚|| | d	d
«      xs t         | dd«      dœS )zYResolve SelfHostedOIDCProvider kwargs; the skip reason names BOTH configuration surfaces.c                ó:   •— t        | ‰j                  |«      «      S rŒ   )r   r4   )Úenv_nameÚcfg_keyÚoidc_cfgs     €r-   Úsettingz_settings.<locals>.setting  s   ø€ Ü! (¨H¯L©L¸Ó,AÓBÐBr/   ÚHERMES_DASHBOARD_OIDC_ISSUERr=   ÚHERMES_DASHBOARD_OIDC_CLIENT_IDrN   u:  Self-hosted OIDC dashboard auth is not configured. Set both an issuer and a client_id â€” either as env vars (HERMES_DASHBOARD_OIDC_ISSUER + HERMES_DASHBOARD_OIDC_CLIENT_ID) or under dashboard.oauth.self_hosted.{issuer,client_id} in config.yaml â€” or pass --insecure to skip the OAuth gate entirely. (issuer set: z; client_id set: r–   ÚHERMES_DASHBOARD_OIDC_SCOPESr9   Ú#HERMES_DASHBOARD_OIDC_CLIENT_SECRETr:   )r=   rN   r9   r:   )rÒ   r   rÓ   r   rš   r   )rÏ   r   ÚboolrD   )rÕ   r=   rN   rÔ   s      @r-   Ú	_settingsrÛ     s�   ø€ ä)Ó+€HõCñ Ð3°XÓ>€FÙÐ9¸;ÓG€IÙ™Ýô �F�|œT )�_ð.ó/ð 	/ð  yÙÐ8¸(ÓCÒVÄá Ð!FÈÓXñ	Zð Zr/   c           
     ó®   — da t        | t        t        t        t
        «      \  }a |�/t        j                  d|d   |d   |d   t        |d   «      «       yy)zIRegister :class:`SelfHostedOIDCProvider` when issuer + client_id are set.r   Nzedashboard-auth-self-hosted: registered provider (issuer=%s, client_id=%s, scopes=%r, confidential=%s)r=   rN   r9   r:   )r    r   rd   rÎ   r8   rÛ   ÚinforÚ   )ÚctxÚkws     r-   Úregisterrà   1  s]   € ð ÐÜ,¨S´&¼$Ô@VÔXaÓbÑ€BÐØ	€~Ü�‰ØsØˆx‰L˜"˜[™/¨2¨h©<¼¸bÀÑ>QÓ9Rõ	Tð r/   )úhermes_cli.dashboard_authÚDashboardAuthProvider)rá   ÚInvalidCodeError)rá   ÚRefreshExpiredError)rá   Úclassify_jwks_lookup_error)râ   rã   rä   rå   c                óÌ   — t         j                  | «      }|€t        dt        ›d| ›�«      ‚dd l}ddlm}  |t        | g|¢­Ž  t        |j                  |d   «      |d   «      S )Nzmodule z has no attribute r   )Ú	warn_onceé   )	Ú_PLUGIN_COMPAT_LAZYr4   ÚAttributeErrorrÄ   Ú	importlibÚhermes_cli.plugin_compatrç   ÚgetattrÚimport_module)r¼   Útargetrë   rç   s       r-   Ú__getattr__rð   M  se   € Ü ×$Ñ$ TÓ*€FØ€~Ü˜w¤x lÐ2DÀTÀHÐMÓNÐNÛÝ2ÙŒh˜Ð&˜vÓ&Ü�9×*Ñ*¨6°!©9Ó5°v¸a±yÓAÐAr/   )r*   r   r+   r   rš   r   )r*   r   rš   Útuple)rš   Údict)rš   rÂ   )7rÇ   Ú
__future__r   rt   ÚloggingrI   r�   Úurllib.parser%   Útypingr   r   r   r`   Úhermes_cli.dashboard_authr   r   r	   Úplugins.dashboard_auth._sharedr
   r   rb   r   r   r   r   r   r   r   r   r   r   r   r   Ú	getLoggerrÄ   rd   rÎ   rD   r±   r›   rŽ   r    Ú__annotations__r.   r6   r8   rÏ   rÛ   rà   ÚhashlibÚosÚsecretsré   rð   rÉ   r/   r-   ú<module>rþ      sÙ   ðòõ #ã Û Û Û Û ß &Ñ &ã ç HÑ H÷÷ ÷ ÷ ð  
ˆ×	Ñ	˜8Ó	$€Ø#€ð )€ð
 PÐ àÐ ð  Ð àÐ �#Ó ó^óBôD&Ð-ô D&óRRóZó0Tó Û 	Û ð TØIØOØ"]ñ	Ð óBr/   