@redocly/redoc-reef 0.140.0-next.2 → 0.140.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/server/constants/common.d.ts +1 -0
- package/dist/server/constants/common.js +1 -1
- package/dist/server/web-server/auth.d.ts +1 -0
- package/dist/server/web-server/auth.js +3 -3
- package/dist/server/web-server/routes/auth.js +1 -1
- package/dist/server/web-server/routes/mcp-routes/mcp-consent-page.d.ts +2 -2
- package/dist/server/web-server/routes/mcp-routes/mcp-consent-page.js +18 -18
- package/dist/server/web-server/routes/mcp-routes/mcp-oauth.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @redocly/redoc-reef
|
|
2
2
|
|
|
3
|
+
## 0.140.0-next.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d7d79acc8df: Extended how long MCP clients stay signed in to the authenticated Docs MCP server from 24 hours to 30 days when the identity provider issues refresh tokens.
|
|
8
|
+
Without refresh tokens, sessions still expire after 24 hours.
|
|
9
|
+
- 95be56a3591: Fixed an issue where Visual review in Reunite showed a sign-in redirect loop for login-protected projects in browsers that block third-party cookies, such as Safari, Firefox, and Brave.
|
|
10
|
+
- 95be56a3591: Improved the security of the Reunite Visual review sign-in for login-protected projects: Reunite no longer passes the session token in the URL.
|
|
11
|
+
|
|
3
12
|
## 0.140.0-next.2
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -19,6 +19,7 @@ export declare const JWT_SECRET_KEY: string;
|
|
|
19
19
|
export declare const MAX_AUTH_TOKEN_COOKIE_BYTES: number;
|
|
20
20
|
export declare const MCP_ACCESS_TOKEN_TTL_SEC: number;
|
|
21
21
|
export declare const MCP_REFRESH_SESSION_LIFETIME_SEC: number;
|
|
22
|
+
export declare const MCP_CLAIMS_ONLY_SESSION_LIFETIME_SEC: number;
|
|
22
23
|
export declare const I18N_DIR_NAME = "@i18n";
|
|
23
24
|
export declare const L10N_DIR_NAME = "@l10n";
|
|
24
25
|
export declare const RESERVED_DIR_NAMES: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ServerRoutes as n}from"../../constants/api.js";import{USER_THEME_ALIAS as A}from"../../constants/common.js";import{envConfig as _}from"../config/env-config.js";const N="runtime",x="_scorecard",a="page-data",
|
|
1
|
+
import{ServerRoutes as n}from"../../constants/api.js";import{USER_THEME_ALIAS as A}from"../../constants/common.js";import{envConfig as _}from"../config/env-config.js";const N="runtime",x="_scorecard",a="page-data",M="/assets",U="/_spec",P="/_bundle",l="static",i="static-data",m="redocly:::linkOriginal",F="@",K="default",H=new RegExp("((?!@i18n\\/|@l10n\\/)\\/?@.*?(?=\\/))","g"),Y="@redocly/theme/plugin.js",d="styles.css",B=["@theme/plugin.js","@theme/plugin.cjs","@theme/plugin.mjs"],I="test_secret_key",G=_.JWT_SECRET_KEY||I,S=4096,L=196,u=S-L,g=3600,W=720*60*60,h=1440*60,c="@i18n",r="@l10n",k=[A,c,r,"@api","@mcp"],J="https://redocly.com/auth/aud",f="catalog.json",w="scorecard.json",X="page-count.json",j="sitemap.xml",V="llms.txt",o=_.PROJECT_ID,$=_.PROJECT_SLUG,R=_.ORGANIZATION_ID,v=_.ORG_SLUG,t=_.BH_API_URL,E=t&&R&&o?`${t}/orgs/${R}/projects/${o}`:void 0,C=t?`${t}/entitlements/.well-known/jwks`:"https://app.cloud.redocly.com/api/entitlements/.well-known/jwks",y=_.ENTITLEMENTS_JWKS_CDN_URL||C,b=E?`${E}/feedback`:"",Z=E?`${E}/ai-agentic-conversations`:"",q=E?`${E}/ai-assistant-escalations`:"",z=E?`${E}/ai-assistant-feedback`:"",Q=E?`${E}/semantic-search`:"",__="no-store",E_=365*24*60*60,t_="X-Redocly-Cache-Control",o_="no-cache",R_="public, s-maxage=86400, max-age=10, must-revalidate",e_=_.AUTH_URL,s_="Developer Documentation",e=_.REDOCLY_SSR_RENDER_MODE==="worker"?"worker":"main",T_=e==="main"?0:_.REDOCLY_SSR_WORKERS_MIN!=null?Number(_.REDOCLY_SSR_WORKERS_MIN):2,n_=e==="main"?1:_.REDOCLY_SSR_WORKERS_MAX!=null?Number(_.REDOCLY_SSR_WORKERS_MAX):5,A_="-----BEGIN CERTIFICATE-----",I_="-----END CERTIFICATE-----",S_=!_.isDevelopMode&&_.REDOCLY_TELEMETRY!=="off"||_.REDOCLY_TELEMETRY==="on",L_="http://localhost:4318/v1/traces",c_=[n.HEALTH],s=_.SERVER_EDITOR_APP_URL||"http://127.0.0.1:3000",r_=_.REDOCLY_CORS_ORIGINS?_.REDOCLY_CORS_ORIGINS.split(",").map(T=>T.trim()).filter(Boolean).concat(s):[s],C_="markdown/partials",O_="markdown/partials-deps";export{q as AI_ASSISTANT_ESCALATIONS_API_URL,z as AI_ASSISTANT_FEEDBACK_API_URL,r_ as ALLOWED_CORS_ORIGINS,J as ALTERNATIVE_AUD_CLAIM_NAME,Z as ASK_AI_API_URL,e_ as AUTH_URL,A_ as BEGIN_CERTIFICATE_DELIMITER,E as BH_API_PROJECTS_URL,__ as CACHE_CONTROL_NO_STORE_HEADER_VALUE,o_ as CACHE_CONTROL_PUBLIC_HTML,f as CATALOG_OUTPUT_FILE_NAME,E_ as DEFAULT_IMMUTABLE_CACHE_MAX_AGE,B as DEFAULT_PROJECT_PLUGIN_PATHS,Y as DEFAULT_THEME_PLUGIN_PATH,s_ as DEFAULT_TITLE,K as DEFAULT_VERSION_NAME,U as DEPRECATED_PUBLIC_API_DEFINITIONS_FOLDER,I_ as END_CERTIFICATE_DELIMITER,y as ENTITLEMENTS_JWKS_URL,I as FALLBACK_JWT_SECRET_KEY,b as FEEDBACK_API_URL,c as I18N_DIR_NAME,G as JWT_SECRET_KEY,r as L10N_DIR_NAME,m as LINK_ORIGINAL_ATTR_NAME,V as LLMS_TXT_FILE_NAME,C_ as MARKDOC_PARTIALS_DATA_KEY,O_ as MARKDOC_PARTIALS_DEPS_KEY,u as MAX_AUTH_TOKEN_COOKIE_BYTES,g as MCP_ACCESS_TOKEN_TTL_SEC,h as MCP_CLAIMS_ONLY_SESSION_LIFETIME_SEC,W as MCP_REFRESH_SESSION_LIFETIME_SEC,R as ORG_ID,v as ORG_SLUG,c_ as OTEL_EXCLUDED_ROUTES,L_ as OTEL_TRACES_DEV_URL,X as PAGE_COUNT_OUTPUT_FILE_NAME,a as PAGE_DATA_FOLDER,o as PROJECT_ID,$ as PROJECT_SLUG,P as PUBLIC_API_DEFINITIONS_FOLDER,M as PUBLIC_ASSETS_FOLDER,l as PUBLIC_STATIC_FOLDER,k as RESERVED_DIR_NAMES,N as RUNTIME_RESOURCES_DIR,x as SCORECARD_CLASSIC_FOLDER,w as SCORECARD_OUTPUT_FILE_NAME,Q as SEMANTIC_SEARCH_API_URL,s as SERVER_EDITOR_APP_URL,j as SITEMAP_FILE_NAME,e as SSR_RENDER_MODE,n_ as SSR_WORKERS_MAX,T_ as SSR_WORKERS_MIN,i as STATIC_DATA_FOLDER,d as STYLES_FILE,S_ as TELEMETRY_ENABLED,H as VERSIONED_FOLDER_REGEXP_MATCH,F as VERSION_SEPARATOR,t_ as X_REDOCLY_CACHE_CONTROL_HEADER,R_ as X_REDOCLY_CACHE_CONTROL_PUBLIC_HTML};
|
|
@@ -161,6 +161,7 @@ export declare const UNKNOWN_KID_RETRY_MS = 60000;
|
|
|
161
161
|
export declare function getOidcMetadata(idpId: string, providerConfig: OidcProviderConfig): Promise<OidcIssuerMetadata>;
|
|
162
162
|
export declare function discoverJwks(ssoConfig: SsoConfig): Promise<void>;
|
|
163
163
|
export declare function getRedoclyTokenPayload(token: string): Promise<Record<string, unknown>>;
|
|
164
|
+
export declare function exchangeTokenLoginCode(code: string): Promise<string | undefined>;
|
|
164
165
|
export declare function isRedoclySso(config: OidcProviderConfig): boolean;
|
|
165
166
|
export declare function verifySAMLResponse(responseXml: string, cert: string): Promise<boolean>;
|
|
166
167
|
export declare function orderTeamsRbacFirst(teams: string[], rbacConfig: RbacConfig | undefined, teamsMapping: Record<string, string> | undefined): string[];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import"../node-crypto-polyfill.js";import{DOMParser as j}from"@xmldom/xmldom";import{SignedXml as
|
|
1
|
+
import"../node-crypto-polyfill.js";import{DOMParser as j}from"@xmldom/xmldom";import{SignedXml as ee}from"xml-crypto";import te from"xpath";import{deflateSync as ne,inflateSync as re}from"fflate";import{createHash as oe}from"crypto";import{ulid as se}from"ulid";import{AuthProviderType as f,DEFAULT_TEAM_CLAIM_NAME as U}from"@redocly/config";import{ALTERNATIVE_AUD_CLAIM_NAME as ie,AUTH_URL as K,JWT_SECRET_KEY as y,MAX_AUTH_TOKEN_COOKIE_BYTES as ae}from"../constants/common.js";import{ensureArray as R}from"../../utils/array/ensure-array.js";import{envConfig as ce}from"../config/env-config.js";import{decryptClaimValue as ue}from"../utils/crypto/decrypt-claim-value.js";import{decryptAesGcm as de}from"../utils/crypto/decrypt-aes-gcm.js";import{encryptAesGcm as le}from"../utils/crypto/encrypt-aes-gcm.js";import{getPathPrefix as me,withPathPrefix as pe,withoutPathPrefix as fe}from"@redocly/theme/core/utils";import{ServerRoutes as V}from"../../constants/api.js";import{AuthCookieNames as I,DEFAULT_AUTHENTICATED_TEAM as he,REQUIRED_OIDC_SCOPES as $}from"../../constants/common.js";import{appendQueryParams as ye}from"../../utils/url/append-query-params.js";import{parsePreviewUrl as B}from"../../utils/url/parse-preview-url.js";import{logger as O}from"../tools/notifiers/logger.js";import{randomString as Ae}from"../utils/crypto/random-string.js";import{randomUUID as M}from"../utils/crypto/random-uuid.js";import{AlgorithmTypes as l,JwtTokenExpired as _e}from"./jwt/types.js";import*as p from"./jwt/jwt.js";import{InternalTokenTypes as g}from"./jwt/token-types.js";import{parseTeamClaimToArray as we}from"../utils/index.js";import{parseTeamNameTemplate as Se}from"../utils/rbac-expand.js";import{arrayBufferToBase64 as Te,decodeBase64 as F,encodeBase64URL as ge,urlSafeBase64 as H}from"./jwt/encode.js";import{formatSamlCertificate as xe}from"./utils/format-saml-certificate.js";const Ie=new Set(Object.values(g));function x(e){return e?.type===f.OIDC}function Ee(e){return e?.type===f.SAML2}async function Mt(e,t){if(x(t))return ke(e,t);if(Ee(t))return Re(e,t)}async function ke(e,t){const n=await Q(e,t),r=new Set((t.scopes||[]).concat($)),o=t.authorizationRequestCustomParams||{};return{type:f.OIDC,idpId:e,name:"OAuth provider",authorizationEndpoint:n.authorization_endpoint,clientId:t.clientId,responseType:"code",scope:Array.from(r).join(" "),extraParams:o,pkce:t.pkce}}function Re(e,t){return{type:f.SAML2,idpId:e,name:"SAML2 provider",ssoUrl:t.ssoUrl,issuerId:t.issuerId,entityId:t.entityId||t.issuerId}}function z(e){return Array.from(new Set((e.scopes||[]).concat($))).join(" ")}async function W(e,t,n){return await fetch(e,{method:"POST",body:new URLSearchParams({client_id:t.clientId,...t.clientSecret?{client_secret:t.clientSecret}:{},...n}).toString(),headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"}}).then(r=>r.json())}async function Lt(e,t,n,r,o={}){return await W(e,r,{scope:z(r),code:t,redirect_uri:Y(n),grant_type:"authorization_code",...o})}async function Nt(e,t,n){return await W(e,n,{scope:z(n),grant_type:"refresh_token",refresh_token:t})}function Oe(e,{authorizationEndpoint:t,clientId:n,responseType:r,scope:o,extraParams:s,idpId:i,pkce:c},d,h,u){if(!t||!n||!r||!o)return{loginUrl:void 0};const a=new URL(t),_=u?.redirectUriOverride??`${e}${pe(V.OIDC_CALLBACK)}`,S={state:M(),idpId:i,redirectUri:_,redirectTo:d,branch:u?.branchOverride??De(e),inviteCode:h,source:u?.sourceOverride??"portal",uiLocales:u?.uiLocales},T={};if(c){const A=H(Ae(50)),E=H(oe("sha256").update(A).digest("base64")),k="S256";a.searchParams.append("code_challenge",E),a.searchParams.append("code_challenge_method",k),T.code_verifier={value:A,options:{secure:!0,httpOnly:!0,expires:new Date(Date.now()+1e3*60*10),path:me()||"/"}}}a.searchParams.append("client_id",n),a.searchParams.append("scope",o),a.searchParams.append("response_type",r),a.searchParams.append("redirect_uri",Y(_)),a.searchParams.append("state",ge(JSON.stringify(S))),u?.uiLocales&&a.searchParams.append("ui_locales",u.uiLocales);for(const A in s)s[A]!==void 0&&a.searchParams.append(A,s[A]);return{loginUrl:a.toString(),cookies:T}}function Dt(e,t,n,r){const o=new URL(e);return n&&(o.searchParams.append("post_logout_redirect_uri",t),r&&o.searchParams.append("state",r),o.searchParams.append("id_token_hint",n)),o.toString()}function Me(e,t){const n=R(t?.audience??[]);return!n.length||J(e,n)}function J(e,t){const n=[e.aud,e[ie]].flat();return t.some(r=>r!==""&&n.includes(r))}function Le(e,t){return!t||!x(t)?!0:Me(e,t)&&Ne(e,t)}function Ne(e,t){return!x(t)||!Ye(t)||!t.clientId?!0:R(e.aud||[]).includes(t.clientId)}function Ct(e){if(!e)return!1;try{return p.decode(e).header.alg===l.RS256}catch{return!1}}async function bt(e){const t=Math.floor(Date.now()/1e3),n=t+(e.ttlSec??600);return p.sign({type:g.MCP_AUTHORIZATION_CODE,client_id:e.clientId,redirect_uri:e.redirectUri,id_token_enc:le(e.idToken,y),...e.idpId?{idp_id:e.idpId}:{},...e.idpAccessTokenEnc?{idp_access_token_enc:e.idpAccessTokenEnc}:{},...e.idpRefreshTokenEnc?{idp_refresh_token_enc:e.idpRefreshTokenEnc}:{},...e.codeChallenge?{code_challenge:e.codeChallenge}:{},...e.codeChallengeMethod?{code_challenge_method:e.codeChallengeMethod}:{},iat:t,exp:n},y,l.HS256)}async function Pt(e){await p.verify(e,y,l.HS256);const{payload:t}=p.decode(e);if(t.type!==g.MCP_AUTHORIZATION_CODE)throw new Error("Invalid authorization code type");if(!t.client_id||!t.redirect_uri)throw new Error("Authorization code missing required claims");if(typeof t.exp=="number"&&Date.now()>=t.exp*1e3)throw new Error("Authorization code expired");if(typeof t.id_token_enc!="string")throw new Error("Authorization code missing session");return{...t,id_token:de(t.id_token_enc,y)}}async function vt(e){const t=Math.floor(Date.now()/1e3);return p.sign({type:g.MCP_REFRESH_TOKEN,client_id:e.clientId,...e.idpId?{idp_id:e.idpId}:{},claims:e.claims,...e.idpAccessTokenEnc?{idp_access_token_enc:e.idpAccessTokenEnc}:{},...e.idpRefreshTokenEnc?{idp_refresh_token_enc:e.idpRefreshTokenEnc}:{},session_exp:e.sessionExp,iat:t,exp:e.sessionExp,jti:M()},y,l.HS256)}async function jt(e){await p.verify(e,y,l.HS256);const{payload:t}=p.decode(e);if(t.type!==g.MCP_REFRESH_TOKEN)throw new Error("Invalid refresh token type");if(!t.client_id||typeof t.claims!="object"||t.claims===null)throw new Error("Refresh token missing required claims");if(typeof t.session_exp!="number")throw new Error("Refresh token missing session expiry");return t}function Ut(e){const t=e||se(),n=t.startsWith("mcp_")?t:`mcp_${t}`;return{id:n,object:"mcpSession",uri:`urn:redocly:realm:mcp:session:${n}`}}function Y(e){const t=B(e);if(!t)return e;const n=Ce(t.hostname);if(n)return t.hostname=`previewauth--${n.previewBranch}${n.after}`,q(e,t);const r=G(t.hostname);return r?(t.hostname=`${r.projectSlug}.previewauth.${r.after}`,q(e,t)):e}function De(e){const t=B(e);if(t)return G(t.hostname)?.previewBranch}function q(e,t){const n=e.replace(/^https?:\/\/[^/?#]+/i,"");return`${t.protocol}//${t.host}${fe(n)}`}function G(e){const t=e.split(".preview.",2);if(t.length<2)return null;const[n,r]=t,o=n.indexOf("--");if(o===-1)return null;const s=n.slice(0,o),i=n.slice(o+2);return!s||!i||i.includes(".")?null:{projectSlug:s,previewBranch:i,after:r}}function Ce(e){const t=e.indexOf("."),n=t===-1?e:e.slice(0,t);if(!n.startsWith("preview-"))return null;const r=n.indexOf("--");if(r===-1)return null;const o=n.slice(r+2);if(!o)return null;const s=t===-1?"":e.slice(t);return{previewBranch:o,after:s}}function be(e){return e.type===f.OIDC}function Pe(e){return e.type===f.SAML2}function Kt(e,t,n,r,o){return be(e)?Oe(t,e,n,r,{uiLocales:o}):Pe(e)?ve(t,e,n,r,o):{}}function ve(e,t,n,r,o){const i=`<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
2
2
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
3
3
|
Version="2.0"
|
|
4
4
|
ID="_${M()}"
|
|
5
5
|
IssueInstant="${new Date().toISOString()}"
|
|
6
|
-
AssertionConsumerServiceURL="${e}${
|
|
6
|
+
AssertionConsumerServiceURL="${e}${V.SAML_CALLBACK}"
|
|
7
7
|
AttributeConsumingServiceIndex="0">
|
|
8
8
|
<saml:Issuer>${t.entityId}</saml:Issuer>
|
|
9
9
|
<samlp:NameIDPolicy
|
|
10
10
|
AllowCreate="true"
|
|
11
11
|
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
|
|
12
|
-
</samlp:AuthnRequest>`,c=je(i);return{loginUrl:ye(t.ssoUrl,{SAMLRequest:c,RelayState:JSON.stringify({idpId:t.idpId,redirectTo:n,inviteCode:r,source:"portal",uiLocales:o})})}}function je(e){return Te(
|
|
12
|
+
</samlp:AuthnRequest>`,c=je(i);return{loginUrl:ye(t.ssoUrl,{SAMLRequest:c,RelayState:JSON.stringify({idpId:t.idpId,redirectTo:n,inviteCode:r,source:"portal",uiLocales:o})})}}function je(e){return Te(ne(new TextEncoder().encode(e)).buffer)}function Vt(e){const t=F(e);if(t.startsWith("<samlp:Response")||t.indexOf("<saml2p:Response")>-1)return t;const n=re(new Uint8Array(atob(e).split("").map(r=>r.charCodeAt(0))));return new TextDecoder().decode(n)}function $t(e){try{return JSON.parse(F(e||""))}catch{throw new Error("Invalid OAuth2 state")}}function Bt(e){const t=new j().parseFromString(e,"application/xml"),r=m(t,"//*[local-name(.)='StatusCode']/@Value")[0]?.nodeValue?.endsWith("Success")||!1,s=m(t,"//*[local-name(.)='Response']/@Destination")[0]?.nodeValue||"",i=m(t,"//*[local-name(.)='Assertion']//*[local-name(.)='Issuer']/text()")[0],c=i&&i.nodeValue||void 0,d=m(t,"//*[local-name(.)='Audience']/text()")[0],h=d&&d.nodeValue||void 0,a=m(t,"//*[local-name(.)='Assertion']//*[local-name(.)='X509Certificate']/text()")[0]?.nodeValue||"",_=m(t,"//*[local-name(.)='Subject']//*[local-name(.)='NameID']/text()")[0],S=_&&_.nodeValue||"",T=m(t,"//*[local-name(.)='Subject']//*[local-name(.)='NameID']/@Format")[0],A=T&&T.nodeValue||"",E=m(t,"//*[local-name(.)='Conditions']/@NotOnOrAfter")[0],k=Ue(E),D={},C=m(t,"//*[local-name(.)='AttributeStatement']//*[local-name(.)='Attribute']");if(C.length)for(const b of C){const P=m(b,"./@Name")[0];if(P.nodeValue){const v=m(b,"./*[local-name(.)='AttributeValue']/text()")[0];v?.nodeValue&&(D[P.nodeValue]=v.nodeValue)}}return{uid:S,success:r,expiresAt:k,issuerId:c,entityId:h,attrs:D,cert:a,nameFormat:A,destination:s}}function Ue(e){const t=typeof e?.nodeValue=="string"&&L(Date.parse(e.nodeValue)),n=L(Date.now()),r=L(Date.now()+720*60*1e3);return t?t>n&&t<r?r:t:n}function L(e){return Math.floor(e/1e3)}const N={},w={jwks:{},missedAt:{}},Ke=6e4;function Ve(e){const t=w.jwks[e];return t===void 0?!0:t===null&&Date.now()-(w.missedAt[e]??0)>=Ke}async function Q(e,t){if(!N[e]){const n=t.configurationUrl?await X(t.configurationUrl):t.configuration;N[e]=$e()?Fe(n):n}return N[e]}function $e(){const e=ce.REDOCLY_ENFORCE_RESIDENCY;return!!e&&e.includes("host.docker.internal")}const Be=["authorization_endpoint","end_session_endpoint"];function Fe(e){if(typeof e!="object"||e===null)return e;const t={...e};for(const n of Object.keys(t)){if(Be.includes(String(n)))continue;const r=t[n];typeof r=="string"&&r.includes("://localhost")&&(t[n]=r.replace("://localhost","://host.docker.internal"))}return t}const He=["kty","n","e","crv","x","y"];function ze(e,t){return He.every(n=>e[n]===t[n])}async function We(e){const t={};for(const n of Object.keys(e)){const r=e[n];if(!x(r))continue;const o=await Q(n,r);if(o.jwks_uri){const s=await X(o.jwks_uri);for(const i of s.keys){const c=t[i.kid],d=c&&ze(c,i)?[...c.idpIds,n]:[n];t[i.kid]={...i,idpIds:d},w.jwks[i.kid]=t[i.kid]}}}}function Je(e,t,n){const r=w.jwks[e]?.idpIds??[];return r.find(o=>{const s=n[o];if(!x(s))return!1;const i=R(s.audience??[]);return J(t,i.length?i:[s.clientId])})??r.at(-1)}async function X(e){return fetch(e,{headers:{Accept:"application/json"}}).then(t=>t.json())}async function Ft(e){return fetch(`${K}/oidc/userinfo`,{headers:{Accept:"application/json",Authorization:`Bearer ${e}`}}).then(t=>t.status===200?t.json():void 0).catch(()=>{})}async function Ht(e){return fetch(`${K}/api/auth/organization-session/login-code/exchange`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify({code:e})}).then(t=>t.status===200?t.json():void 0).then(t=>typeof t?.token=="string"?t.token:void 0).catch(()=>{})}function Ye(e){if(!e.configurationUrl)return!1;const t=new URL(e.configurationUrl);return["localhost","127.0.0.1","blueharvest.cloud","bhstage.cloud","cloud.redocly.com","beta.redocly.com","cloud.eu.redocly.com","beta.eu.redocly.com"].some(r=>qe(t.hostname,r))}function qe(e,t){return e===t||e.endsWith(`.${t}`)}async function zt(e,t){const n=new j().parseFromString(e,"application/xml"),r=m(n,"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];if(!r)throw new Error("Cannot find Signature in the SAML response");const o=xe(t),s=new ee({publicCert:o});s.loadSignature(r);try{return s.checkSignature(e)}catch{return!1}}function Ge(e){const t=new Set,n=r=>{if(!(!r||typeof r!="object"||Array.isArray(r)))for(const[o,s]of Object.entries(r))typeof s=="string"?t.add(o):n(s)};return n(e),t}function Qe(e,t,n){if(!t)return e;const r=Ge(t),o=s=>{const i=n?.[s]||s;return r.has(i)||!!Se(t,i)};return[...e.filter(o),...e.filter(s=>!o(s))]}async function Wt(e,t,n,r){const o=n?e[n]:void 0,s=Array.isArray(o)?Qe(o.map(String),r?.rbacConfig,r?.teamsMapping):[];let i=s.length;const c=s.length+1;for(let d=0;d<c;d++){const h={...e,idpId:t};n&&s.length&&(h[n]=s.slice(0,i));const u=await p.sign(h,y,l.HS256),a=Buffer.byteLength(u)-ae;if(a<=0)return{token:u,teamsTotal:s.length,teamsKept:i,droppedTeams:s.slice(i)};if(i===0)return null;const _=Buffer.byteLength(JSON.stringify(s.slice(0,i))),S=Math.max(1,_/i*4/3);i=Math.max(0,i-Math.max(1,Math.ceil(a/S)))}return null}function Jt(e,t,n,r){t==="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"&&(e=n["http://schemas.microsoft.com/identity/claims/objectidentifier"]);let o;(t==="urn:oasis:names:tc:SAML:2.0:nameid-format:email"||t==="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")&&(o=e),t==="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"&&e?.match(/^.+@.+/m)&&(o=e);const s=n["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],i=s?.match(/^.+@.+/m);return o=o||n["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]||(i?s:void 0),o=o?.toLowerCase(),{sub:e,given_name:n["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],family_name:n["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"],name:n["http://schemas.microsoft.com/identity/claims/displayname"]||s,email:o,email_verified:!0,teams:r?we(n[r]):[]}}function Z(e,t={}){return e.map(n=>t[n]||n)}async function Yt(e,t){if(!t)return{};const n=t[I.AUTHORIZATION];if(!n)return{};try{const r=p.decode(n);if(typeof r.payload.type=="string"&&Ie.has(r.payload.type))return{};if(r.header.alg===l.RS256){const h=Ve(r.header.kid);h&&await We(e);const u=w.jwks[r.header.kid];if(!u)return h&&(w.missedAt[r.header.kid]=Date.now()),w.jwks[r.header.kid]=null,{};await p.verify(n,u,l.RS256)}else await p.verify(n,y,l.HS256);const s=r.header.alg===l.RS256?Je(r.header.kid,r.payload,e):r.payload.idpId,i=e[s]||{};if(r.header.alg===l.RS256&&typeof r.payload.exp!="number")return O.warn("Rejected id_token: it has no exp claim"),{};if(r.header.alg===l.RS256&&!Le(r.payload,i))return O.warn("Rejected id_token: it was issued for another project"),{};const c=et(i),d=Ze(i);return{...r.payload,email:r.payload.email?.toLowerCase(),idpId:s,teams:Array.from(new Set([...Z(r.payload.teams||[],d),..."defaultTeams"in i&&i.defaultTeams||[],...Z("teamsClaimName"in i&&r.payload[c||""]||[],d),he])),name:Xe(r.payload),isAuthenticated:!0,idpAccessToken:ue(r.payload.idp_access_token,y)||t[I.IDP_ACCESS_TOKEN],federatedAccessToken:t[I.FEDERATED_ACCESS_TOKEN],federatedIdToken:t[I.FEDERATED_ID_TOKEN],authCookie:n}}catch(r){r instanceof _e||O.error("Malformed JWT token: %s",r.message)}return{}}function Xe(e){return(e.firstName&&e.lastName?`${e.firstName} ${e.lastName}`:e.name||e.given_name||e.firstName||e.lastName)||e.email}function Ze(e){switch(e.type){case f.SAML2:return e.teamsAttributeMap;case f.OIDC:return e.teamsClaimMap;default:return}}function et(e){switch(e.type){case f.SAML2:return e.teamsAttributeName??U;case f.OIDC:return e.teamsClaimName;default:return U}}function m(e,t){return te.select(t,e)||[]}export{Ke as UNKNOWN_KID_RETRY_MS,Kt as buildLoginUrl,Oe as buildOidcLoginUrl,Dt as buildOidcLogoutUrl,ve as buildSAML2LoginUrl,bt as createMcpAuthorizationCode,vt as createMcpRefreshToken,Ut as createMcpSessionResource,Vt as decodeSamlResponse,We as discoverJwks,je as encodeSAML2,Ht as exchangeTokenLoginCode,Jt as extractUserClaims,Mt as getAuthProviderLoginParams,ke as getOidcLoginParams,Q as getOidcMetadata,Ft as getRedoclyTokenPayload,Re as getSaml2LoginParams,Yt as getUserParamsFromCookies,Xe as getUsernameFromPayload,Me as idTokenAudienceIsAllowed,Le as idTokenIsForThisProject,Ct as isIdpIssuedToken,x as isOidcProviderConfig,Ye as isRedoclySso,Ee as isSaml2ProviderConfig,Lt as oidcExchangeCodeForToken,w as oidcJwksCache,N as oidcMetadataCache,Nt as oidcRefreshTokenGrant,Qe as orderTeamsRbacFirst,$t as parseOidcState,De as parsePreviewBranch,Bt as parseSamlResponse,Y as rewritePreviewAuthRedirectUri,Wt as signTokenWithinCookieSizeLimit,Pt as verifyMcpAuthorizationCode,jt as verifyMcpRefreshToken,zt as verifySAMLResponse};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{setCookie as L,deleteCookie as P}from"hono/cookie";import{AuthProviderType as Y,DEFAULT_TEAM_CLAIM_NAME as Q}from"@redocly/config";import{withPathPrefix as w,getPathPrefix as A}from"@redocly/theme/core/utils";import{compareURIs as x}from"../../../utils/url/compare-uris.js";import{ensureArray as ee}from"../../../utils/array/ensure-array.js";import{ALTERNATIVE_AUD_CLAIM_NAME as re,JWT_SECRET_KEY as U,MAX_AUTH_TOKEN_COOKIE_BYTES as N,ORG_SLUG as oe,ORG_ID as te}from"../../constants/common.js";import{AUTH_SEGMENT as ne,ServerRoutes as k}from"../../../constants/api.js";import{AuthCookieNames as _,DEFAULT_COOKIE_EXPIRATION as B}from"../../../constants/common.js";import{sanitizeRedirectPathname as H}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as y}from"../../telemetry/index.js";import{envConfig as K}from"../../config/env-config.js";import{fetchWithClient as ie}from"../../utils/fetch-with-client.js";import{clearAuthCookies as z,clearPostLogoutCookie as v,hasPostLogoutCookie as se,setPostLogoutCookie as j}from"../../utils/cookie.js";import{getAuthProviderLoginParams as ae,isOidcProviderConfig as b,isSaml2ProviderConfig as de,oidcExchangeCodeForToken as le,buildLoginUrl as ce,decodeSamlResponse as ue,extractUserClaims as pe,parseSamlResponse as ge,parseOidcState as fe,verifySAMLResponse as me,getUsernameFromPayload as he,buildOidcLogoutUrl as Ce,isIdpIssuedToken as Ie,getOidcMetadata as J,getRedoclyTokenPayload as _e,idTokenAudienceIsAllowed as Ae,isRedoclySso as Oe,rewritePreviewAuthRedirectUri as Re,parsePreviewBranch as Te,buildOidcLoginUrl as Le,createMcpSessionResource as M,signTokenWithinCookieSizeLimit as G}from"../auth.js";import*as $ from"../jwt/jwt.js";import{AlgorithmTypes as E}from"../jwt/types.js";import{handleErrorPageRender as we}from"../utils.js";import{encodeBase64URL as ye}from"../jwt/encode.js";import{resolveUiLocalesForIdpLogin as Me}from"./helpers/resolve-ui-locales-for-idp-login.js";async function Ge(n,e){if(!e.devLogin||K.isProductionEnv)return n.newResponse(null,404,{});const{password:r,...a}=await n.req.json(),o=await $.sign({...a,name:a.username||a.email||"Unknown"},U,E.HS256);return L(n,_.AUTHORIZATION,o,{path:A()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),v(n),n.newResponse(null,200,{})}function Ze(){return async n=>{const e=n.get("logger"),r=encodeURIComponent(n.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${k.LOGIN}/?error=${encodeURIComponent(r)}`;return n.newResponse(null,301,{Location:a})}}function Z(n){if(!n||!n.includes(k.MCP_CALLBACK))return null;try{const e=n.split("/"),r=e[e.length-1];if(r){const a=Buffer.from(r,"base64url").toString("utf-8");return JSON.parse(a).mcpSessionId||null}}catch{}return null}function Ve(n){return async e=>{const r=e.get("logger"),a=n.getConfig().ssoDirect;let o;try{o=fe(e.req.query("state"))}catch{return r.warn("OIDC login error: Invalid OAuth2 state"),e.text("Bad request: Invalid OAuth2 state",400)}const C=o.idpId,i=o.source==="mcp"||o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(k.MCP_CALLBACK),u=i?Z(typeof o.redirectTo=="string"?o.redirectTo:void 0):null,s=a?.[C];if(!b(s))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const p=await J(C,s);if(a&&!p.token_endpoint){const m="Invalid OIDC configuration: token_endpoint is required";return r.error(`OIDC login error: ${m}`),e.text(m,500)}try{const m=p.token_endpoint,f=e.req.query("code"),h=e.req.query("error");if(h)return i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:`OIDC error: ${h}`,error_details:e.req.query("error_description")||null}]),we(e,n,{slug:"/"},403,"403OIDC");if(!f){const g="Code is expected but not present";return r.error(`OIDC login error: ${g}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:g,error_details:null}]),new Response(`Forbidden: ${g}`,{status:403})}const R=typeof o.redirectUri=="string"?o.redirectUri:new URL(w(k.OIDC_CALLBACK),e.req.url).toString(),I=e.get("cookies")?.code_verifier,d=await le(m,f,R,s,{...s.tokenRequestCustomParams,...I?{code_verifier:I}:{}});if(d.error)return r.error(`Error from OIDC provider: "${d.error}"`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:`Token exchange error: ${d.error}`,error_details:d.error_description||null}]),e.text(`Forbidden: ${d.error_description||d.error}`,403);if(!d?.id_token){const g="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${g}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:g,error_details:null}]),new Response(`Forbidden: ${g}`,{status:403})}const{payload:l,header:T}=$.decode(d.id_token),t=T.alg===E.RS256;if(!Ae(l,s)){const g="No valid audience found in id_token";return r.error(`OIDC login error: ${g}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:g,error_details:null}]),new Response(`Forbidden: ${g}`,{status:403})}let c=t?d.id_token:await $.sign({...l,idpId:C},U,E.HS256);if(Buffer.byteLength(c)>N){const g=await G(l,C,s.teamsClaimName,{rbacConfig:n.getConfig().access?.rbac,teamsMapping:s.teamsClaimMap});if(!g){const S=`The id_token claims are too large for the session cookie: ${Buffer.byteLength(JSON.stringify(l))} bytes of claims`;return r.error(`OIDC login error: ${S}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:S,error_details:null}]),e.text(`Forbidden: ${S}`,403)}g.teamsKept<g.teamsTotal&&r.warn(`OIDC login: the "${s.teamsClaimName}" claim is too large for the session cookie size limit; kept ${g.teamsKept} of ${g.teamsTotal} values, with teams from the RBAC configuration first; RBAC ignores the dropped teams: ${JSON.stringify(g.droppedTeams)}`),c=g.token}he(l)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const D=s?.tokenExpirationTime?Date.now()+s.tokenExpirationTime*1e3:l.exp*1e3||Date.now()+B*1e3;if(s.introspectEndpoint){const g=await fetch(s.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:d.access_token})});if(g.ok){const q=(await g.json()).ext?.federatedIdentity;q&&(L(e,_.FEDERATED_ACCESS_TOKEN,q.access_token||"",{path:A()||"/",httpOnly:!1,expires:new Date(D)}),L(e,_.FEDERATED_ID_TOKEN,q.id_token||"",{path:A()||"/",httpOnly:!1,expires:new Date(D)}))}else r.warn(`OIDC introspect error: ${g.statusText}`)}if(L(e,_.AUTHORIZATION,c,{path:A()||"/",httpOnly:!0,expires:new Date(D)}),c!==d.id_token&&Buffer.byteLength(d.id_token)<=N?L(e,_.IDP_ID_TOKEN,d.id_token||"",{path:A()||"/",httpOnly:!0,expires:new Date(D)}):P(e,_.IDP_ID_TOKEN,{path:A()||"/"}),L(e,_.IDP_ACCESS_TOKEN,d.access_token||"",{path:A()||"/",httpOnly:!0,expires:new Date(D)}),d.refresh_token?L(e,_.IDP_REFRESH_TOKEN,d.refresh_token,{path:A()||"/",httpOnly:!0,expires:new Date(D)}):P(e,_.IDP_REFRESH_TOKEN,{path:A()||"/"}),P(e,"code_verifier",{path:A()||"/"}),v(e),i&&o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(k.MCP_CALLBACK)){const S=`${e.req.url.split("?")[0].replace(k.OIDC_CALLBACK,"")}${o.redirectTo}`;return e.newResponse(null,302,{Location:S})}const V=typeof o.redirectTo=="string"?o.redirectTo:void 0,W=H(new URL(V||"/",e.req.url).pathname),X=e.newResponse(null,302,{Location:W});return r.updateContext({email:l.email,subject:l.sub}),r.info("OIDC login successful"),X}catch(m){const f=m instanceof Error?m.message:String(m),h=m instanceof Error?m.stack:String(m);if(r.error(`OIDC login error: ${f}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:f,error_details:h}]),m.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const O="Something went wrong";return r.error(`OIDC login error: ${O}`),i&&y.sendMcpAuthorizationFailedMessage([{...M(u),error:O,error_details:null}]),e.text(O,500)}}function We(n){return async e=>{const r=e.get("logger"),o=e.get("auth").claims?.idpId,i=n.getConfig().ssoDirect?.[o];if(e.req.method==="POST")return b(i)||(z(e),j(e)),r.info("Logout successful"),e.newResponse(null,200,{});let u;if(b(i)){const s=(await J(o,i)).end_session_endpoint;if(s){const p=new URL(e.req.url),O=e.req.header("x-forwarded-proto")||p.protocol.slice(0,-1)||"https",m=e.req.header("x-forwarded-host")||p.host,f=`${O}://${m}`,h=Te(f),R=h?ye(JSON.stringify({branch:h,postLogoutRedirectUri:`${f}${w(k.POST_LOGOUT)}`})):void 0,I=h?`${Re(f)}/${ne}/logout`:`${f}${w(k.POST_LOGOUT)}`,d=e.get("cookies"),l=d?.[_.AUTHORIZATION],T=d?.[_.IDP_ID_TOKEN]||(Ie(l)?l:void 0);u=Ce(s,I,T,R)}}return r.info("Logout successful"),z(e),j(e),e.newResponse(null,302,{Location:u||w("/")})}}function Xe(n){return async e=>{const r=n.getConfig().access?.logoutReturnUrl,a=r||w("/");return e.newResponse(null,302,{Location:a})}}function Ye(n){return async e=>{const r=e.get("logger"),a=e.req.param("code"),o=K.BH_API_URL,C=(i,u,s)=>i&&u?`${i} ${u.charAt(0)}`:s;try{if(!o)throw new Error("BH_API_URL is not set");const i=n.getConfig().ssoDirect;if(!i||!Object.keys(i).length)return r.warn("Invite no sso configured to handle"),e.redirect(w("/"));const u=await ie(`${o}/public-user-invites/${a}`);if(!u.ok)return u.status===404?(r.warn(`Invite ${a} not found redirect to homepage`),e.redirect(w("/"))):(r.error("Invite error",await u.text()),e.redirect(w("/")));const s=await u.json(),p=new URL(w("/invite"),e.req.url);return p.searchParams.set("code",a),p.searchParams.set("org",s.organization.name),p.searchParams.set("invitedBy",C(s.invitedBy.firstName,s.invitedBy.lastName,s.invitedBy.name)),e.newResponse(null,302,{Location:p.toString()})}catch(i){return r.error("Error processing invite",{error:i,inviteCode:a}),e.text(i.message||"Failed to process invite",400)}}}function Qe(n){return async e=>{const r=e.get("logger"),a=n.getConfig().ssoDirect,o=new URL(e.req.url),C=e.req.query("inviteCode"),i=e.req.header("x-forwarded-proto")||o.protocol.slice(0,-1)||"https",u=e.req.header("x-forwarded-host")||o.host,s=`${i}://${u}`;let p=o.searchParams.get("idpId");const O=o.searchParams.get("redirectTo"),m=Object.keys(a||{})[0];p=p||m;const f=o.searchParams.get("mcp_redirect_uri"),h=!!f;if(!a?.[p]){const c="Invalid idpId";if(r.error(`IdP login error: ${c}`),h){const F=Z(O||void 0);y.sendMcpAuthorizationFailedMessage([{...M(F),error:c,error_details:null}])}return e.text(`Forbidden: ${c}`,403)}const I=Me({localePrefixParam:o.searchParams.get("localePrefix"),l10n:n.getGlobalData()?.l10n}),d=p&&a?await ae(p,a[p]):void 0,l={};for(const c of Object.keys(d?.extraParams||{}))l[c]=o.searchParams.get(c)||d?.extraParams?.[c]||void 0;se(e)&&(l.prompt="login",l.post_logout="1");let T,t={};if(h&&f&&d&&d.type===Y.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${f}`);const c=Le("",{...d,extraParams:l},O,C,{redirectUriOverride:f,sourceOverride:"mcp",branchOverride:void 0,uiLocales:I});T=c.loginUrl,t=c.cookies||{}}else if(d){const c=ce({...d,extraParams:l},s,O,C,I);T=c.loginUrl,t=c.cookies||{}}return Object.keys(t).forEach(c=>{L(e,c,t[c].value,t[c].options)}),r.info(`IdP login initiated for ID '${p}'`),e.newResponse(null,302,{Location:T||new URL(e.req.url).pathname})}}function xe(n){return async e=>{const r=e.get("logger"),a=await e.req.formData(),o=a.get("SAMLResponse"),C=a.get("RelayState");if(typeof o!="string"||typeof C!="string"){const t="SAMLResponse is required";return r.error(`SAML2 login error: ${t}`),e.text(`Bad request: ${t}`,400)}const i=ue(o),{success:u,uid:s,nameFormat:p,attrs:O,issuerId:m,expiresAt:f}=ge(i),{idpId:h,redirectTo:R}=JSON.parse(C);if(!u){const t="SAML2 assertion is not successful";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!f||Math.ceil(Date.now()/1e3)>=f){const t="SAML2 Token Expired";return r.error(`SAML2 login error: ${t}`),e.text(t,401)}const I=n.getConfig().ssoDirect?.[h];if(!I||!de(I)){const t="Cannot find valid IdP";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!(I.issuerId&&m&&x(I.issuerId,m))){const t="IssuerID is misconfigured or untrusted assertions issuer received";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!await me(i,I.x509PublicCert)){const t="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${t}`),e.text(t,401)}const l=pe(s,p,O,I.teamsAttributeName??Q);if(!l.sub){const t="The provider did not return a valid user identity.";return r.error(`SAML2 login error: ${t}`),e.text(t,400)}if(!l.email){const t="The provider did not return a valid user email.";return r.error(`SAML2 login error: ${t}`),e.text(t,400)}let T=await $.sign({...l,idpId:h},U,E.HS256);if(Buffer.byteLength(T)>N){const t=await G({...l},h,"teams",{rbacConfig:n.getConfig().access?.rbac,teamsMapping:I.teamsAttributeMap});if(!t){const c=`The SAML2 claims are too large for the session cookie: ${Buffer.byteLength(JSON.stringify(l))} bytes of claims`;return r.error(`SAML2 login error: ${c}`),e.text(`Forbidden: ${c}`,403)}t.teamsKept<t.teamsTotal&&r.warn(`SAML2 login: the "teams" claim is too large for the session cookie size limit; kept ${t.teamsKept} of ${t.teamsTotal} values, with teams from the RBAC configuration first; RBAC ignores the dropped teams: ${JSON.stringify(t.droppedTeams)}`),T=t.token}return L(e,_.AUTHORIZATION,T,{path:A()||"/",httpOnly:!0,expires:new Date(f*1e3)}),P(e,_.IDP_REFRESH_TOKEN,{path:A()||"/"}),v(e),r.updateContext({email:l.email,subject:l.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:R||"/"})}}function er(n){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),o=w(H(a.pathname)),C=n.getConfig().ssoDirect,i=Object.entries(C||{}).find(([,R])=>b(R)&&Oe(R));if(!(C&&i))return e.newResponse(null,302,{Location:o});const s=e.req.query("token"),p=s&&await _e(s);if(!p)return e.newResponse(null,302,{Location:o});if(!ee(p[re]||[]).some(R=>R===oe||R===te))return e.newResponse(null,302,{Location:o});const f=await $.sign({...p,idpId:i?.at(0)},U,E.HS256),h=Date.now()+B*1e3;return L(e,_.AUTHORIZATION,f,{path:A()||"/",httpOnly:!0,expires:new Date(h),sameSite:"None",secure:!0}),P(e,_.IDP_REFRESH_TOKEN,{path:A()||"/"}),v(e),r.info("Token login successful"),e.newResponse(null,302,{Location:o})}}export{Ge as authorizeHandler,Qe as idpLoginHandler,Ye as inviteHandler,We as logoutHandler,Ve as oidcCallbackHandler,Xe as postLogoutHandler,Ze as redoclyLoginCallbackHandler,er as redoclyTokenLoginHandler,xe as samlCallbackHandler};
|
|
1
|
+
import{setCookie as T,deleteCookie as P}from"hono/cookie";import{AuthProviderType as Y,DEFAULT_TEAM_CLAIM_NAME as Q}from"@redocly/config";import{withPathPrefix as L,getPathPrefix as O}from"@redocly/theme/core/utils";import{compareURIs as x}from"../../../utils/url/compare-uris.js";import{ensureArray as ee}from"../../../utils/array/ensure-array.js";import{ALTERNATIVE_AUD_CLAIM_NAME as re,JWT_SECRET_KEY as U,MAX_AUTH_TOKEN_COOKIE_BYTES as N,ORG_SLUG as oe,ORG_ID as te}from"../../constants/common.js";import{AUTH_SEGMENT as ne,ServerRoutes as M}from"../../../constants/api.js";import{AuthCookieNames as A,DEFAULT_COOKIE_EXPIRATION as B}from"../../../constants/common.js";import{sanitizeRedirectPathname as H}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as w}from"../../telemetry/index.js";import{envConfig as K}from"../../config/env-config.js";import{fetchWithClient as ie}from"../../utils/fetch-with-client.js";import{clearAuthCookies as z,clearPostLogoutCookie as v,hasPostLogoutCookie as se,setPostLogoutCookie as j}from"../../utils/cookie.js";import{getAuthProviderLoginParams as ae,isOidcProviderConfig as b,isSaml2ProviderConfig as de,oidcExchangeCodeForToken as ce,buildLoginUrl as le,decodeSamlResponse as ue,extractUserClaims as pe,parseSamlResponse as ge,parseOidcState as fe,verifySAMLResponse as me,getUsernameFromPayload as he,buildOidcLogoutUrl as Ce,isIdpIssuedToken as Ie,getOidcMetadata as J,exchangeTokenLoginCode as _e,getRedoclyTokenPayload as Ae,idTokenAudienceIsAllowed as Oe,isRedoclySso as Re,rewritePreviewAuthRedirectUri as Te,parsePreviewBranch as Le,buildOidcLoginUrl as we,createMcpSessionResource as y,signTokenWithinCookieSizeLimit as G}from"../auth.js";import*as $ from"../jwt/jwt.js";import{AlgorithmTypes as E}from"../jwt/types.js";import{handleErrorPageRender as ye}from"../utils.js";import{encodeBase64URL as ke}from"../jwt/encode.js";import{resolveUiLocalesForIdpLogin as Me}from"./helpers/resolve-ui-locales-for-idp-login.js";async function Ze(n,e){if(!e.devLogin||K.isProductionEnv)return n.newResponse(null,404,{});const{password:r,...a}=await n.req.json(),o=await $.sign({...a,name:a.username||a.email||"Unknown"},U,E.HS256);return T(n,A.AUTHORIZATION,o,{path:O()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),v(n),n.newResponse(null,200,{})}function Ve(){return async n=>{const e=n.get("logger"),r=encodeURIComponent(n.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${M.LOGIN}/?error=${encodeURIComponent(r)}`;return n.newResponse(null,301,{Location:a})}}function Z(n){if(!n||!n.includes(M.MCP_CALLBACK))return null;try{const e=n.split("/"),r=e[e.length-1];if(r){const a=Buffer.from(r,"base64url").toString("utf-8");return JSON.parse(a).mcpSessionId||null}}catch{}return null}function We(n){return async e=>{const r=e.get("logger"),a=n.getConfig().ssoDirect;let o;try{o=fe(e.req.query("state"))}catch{return r.warn("OIDC login error: Invalid OAuth2 state"),e.text("Bad request: Invalid OAuth2 state",400)}const I=o.idpId,i=o.source==="mcp"||o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(M.MCP_CALLBACK),u=i?Z(typeof o.redirectTo=="string"?o.redirectTo:void 0):null,s=a?.[I];if(!b(s))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const g=await J(I,s);if(a&&!g.token_endpoint){const m="Invalid OIDC configuration: token_endpoint is required";return r.error(`OIDC login error: ${m}`),e.text(m,500)}try{const m=g.token_endpoint,h=e.req.query("code"),C=e.req.query("error");if(C)return i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:`OIDC error: ${C}`,error_details:e.req.query("error_description")||null}]),ye(e,n,{slug:"/"},403,"403OIDC");if(!h){const p="Code is expected but not present";return r.error(`OIDC login error: ${p}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:p,error_details:null}]),new Response(`Forbidden: ${p}`,{status:403})}const k=typeof o.redirectUri=="string"?o.redirectUri:new URL(L(M.OIDC_CALLBACK),e.req.url).toString(),f=e.get("cookies")?.code_verifier,d=await ce(m,h,k,s,{...s.tokenRequestCustomParams,...f?{code_verifier:f}:{}});if(d.error)return r.error(`Error from OIDC provider: "${d.error}"`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:`Token exchange error: ${d.error}`,error_details:d.error_description||null}]),e.text(`Forbidden: ${d.error_description||d.error}`,403);if(!d?.id_token){const p="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${p}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:p,error_details:null}]),new Response(`Forbidden: ${p}`,{status:403})}const{payload:c,header:R}=$.decode(d.id_token),t=R.alg===E.RS256;if(!Oe(c,s)){const p="No valid audience found in id_token";return r.error(`OIDC login error: ${p}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:p,error_details:null}]),new Response(`Forbidden: ${p}`,{status:403})}let l=t?d.id_token:await $.sign({...c,idpId:I},U,E.HS256);if(Buffer.byteLength(l)>N){const p=await G(c,I,s.teamsClaimName,{rbacConfig:n.getConfig().access?.rbac,teamsMapping:s.teamsClaimMap});if(!p){const S=`The id_token claims are too large for the session cookie: ${Buffer.byteLength(JSON.stringify(c))} bytes of claims`;return r.error(`OIDC login error: ${S}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:S,error_details:null}]),e.text(`Forbidden: ${S}`,403)}p.teamsKept<p.teamsTotal&&r.warn(`OIDC login: the "${s.teamsClaimName}" claim is too large for the session cookie size limit; kept ${p.teamsKept} of ${p.teamsTotal} values, with teams from the RBAC configuration first; RBAC ignores the dropped teams: ${JSON.stringify(p.droppedTeams)}`),l=p.token}he(c)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const D=s?.tokenExpirationTime?Date.now()+s.tokenExpirationTime*1e3:c.exp*1e3||Date.now()+B*1e3;if(s.introspectEndpoint){const p=await fetch(s.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:d.access_token})});if(p.ok){const q=(await p.json()).ext?.federatedIdentity;q&&(T(e,A.FEDERATED_ACCESS_TOKEN,q.access_token||"",{path:O()||"/",httpOnly:!1,expires:new Date(D)}),T(e,A.FEDERATED_ID_TOKEN,q.id_token||"",{path:O()||"/",httpOnly:!1,expires:new Date(D)}))}else r.warn(`OIDC introspect error: ${p.statusText}`)}if(T(e,A.AUTHORIZATION,l,{path:O()||"/",httpOnly:!0,expires:new Date(D)}),l!==d.id_token&&Buffer.byteLength(d.id_token)<=N?T(e,A.IDP_ID_TOKEN,d.id_token||"",{path:O()||"/",httpOnly:!0,expires:new Date(D)}):P(e,A.IDP_ID_TOKEN,{path:O()||"/"}),T(e,A.IDP_ACCESS_TOKEN,d.access_token||"",{path:O()||"/",httpOnly:!0,expires:new Date(D)}),d.refresh_token?T(e,A.IDP_REFRESH_TOKEN,d.refresh_token,{path:O()||"/",httpOnly:!0,expires:new Date(D)}):P(e,A.IDP_REFRESH_TOKEN,{path:O()||"/"}),P(e,"code_verifier",{path:O()||"/"}),v(e),i&&o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(M.MCP_CALLBACK)){const S=`${e.req.url.split("?")[0].replace(M.OIDC_CALLBACK,"")}${o.redirectTo}`;return e.newResponse(null,302,{Location:S})}const V=typeof o.redirectTo=="string"?o.redirectTo:void 0,W=H(new URL(V||"/",e.req.url).pathname),X=e.newResponse(null,302,{Location:W});return r.updateContext({email:c.email,subject:c.sub}),r.info("OIDC login successful"),X}catch(m){const h=m instanceof Error?m.message:String(m),C=m instanceof Error?m.stack:String(m);if(r.error(`OIDC login error: ${h}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:h,error_details:C}]),m.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const _="Something went wrong";return r.error(`OIDC login error: ${_}`),i&&w.sendMcpAuthorizationFailedMessage([{...y(u),error:_,error_details:null}]),e.text(_,500)}}function Xe(n){return async e=>{const r=e.get("logger"),o=e.get("auth").claims?.idpId,i=n.getConfig().ssoDirect?.[o];if(e.req.method==="POST")return b(i)||(z(e),j(e)),r.info("Logout successful"),e.newResponse(null,200,{});let u;if(b(i)){const s=(await J(o,i)).end_session_endpoint;if(s){const g=new URL(e.req.url),_=e.req.header("x-forwarded-proto")||g.protocol.slice(0,-1)||"https",m=e.req.header("x-forwarded-host")||g.host,h=`${_}://${m}`,C=Le(h),k=C?ke(JSON.stringify({branch:C,postLogoutRedirectUri:`${h}${L(M.POST_LOGOUT)}`})):void 0,f=C?`${Te(h)}/${ne}/logout`:`${h}${L(M.POST_LOGOUT)}`,d=e.get("cookies"),c=d?.[A.AUTHORIZATION],R=d?.[A.IDP_ID_TOKEN]||(Ie(c)?c:void 0);u=Ce(s,f,R,k)}}return r.info("Logout successful"),z(e),j(e),e.newResponse(null,302,{Location:u||L("/")})}}function Ye(n){return async e=>{const r=n.getConfig().access?.logoutReturnUrl,a=r||L("/");return e.newResponse(null,302,{Location:a})}}function Qe(n){return async e=>{const r=e.get("logger"),a=e.req.param("code"),o=K.BH_API_URL,I=(i,u,s)=>i&&u?`${i} ${u.charAt(0)}`:s;try{if(!o)throw new Error("BH_API_URL is not set");const i=n.getConfig().ssoDirect;if(!i||!Object.keys(i).length)return r.warn("Invite no sso configured to handle"),e.redirect(L("/"));const u=await ie(`${o}/public-user-invites/${a}`);if(!u.ok)return u.status===404?(r.warn(`Invite ${a} not found redirect to homepage`),e.redirect(L("/"))):(r.error("Invite error",await u.text()),e.redirect(L("/")));const s=await u.json(),g=new URL(L("/invite"),e.req.url);return g.searchParams.set("code",a),g.searchParams.set("org",s.organization.name),g.searchParams.set("invitedBy",I(s.invitedBy.firstName,s.invitedBy.lastName,s.invitedBy.name)),e.newResponse(null,302,{Location:g.toString()})}catch(i){return r.error("Error processing invite",{error:i,inviteCode:a}),e.text(i.message||"Failed to process invite",400)}}}function xe(n){return async e=>{const r=e.get("logger"),a=n.getConfig().ssoDirect,o=new URL(e.req.url),I=e.req.query("inviteCode"),i=e.req.header("x-forwarded-proto")||o.protocol.slice(0,-1)||"https",u=e.req.header("x-forwarded-host")||o.host,s=`${i}://${u}`;let g=o.searchParams.get("idpId");const _=o.searchParams.get("redirectTo"),m=Object.keys(a||{})[0];g=g||m;const h=o.searchParams.get("mcp_redirect_uri"),C=!!h;if(!a?.[g]){const l="Invalid idpId";if(r.error(`IdP login error: ${l}`),C){const F=Z(_||void 0);w.sendMcpAuthorizationFailedMessage([{...y(F),error:l,error_details:null}])}return e.text(`Forbidden: ${l}`,403)}const f=Me({localePrefixParam:o.searchParams.get("localePrefix"),l10n:n.getGlobalData()?.l10n}),d=g&&a?await ae(g,a[g]):void 0,c={};for(const l of Object.keys(d?.extraParams||{}))c[l]=o.searchParams.get(l)||d?.extraParams?.[l]||void 0;se(e)&&(c.prompt="login",c.post_logout="1");let R,t={};if(C&&h&&d&&d.type===Y.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${h}`);const l=we("",{...d,extraParams:c},_,I,{redirectUriOverride:h,sourceOverride:"mcp",branchOverride:void 0,uiLocales:f});R=l.loginUrl,t=l.cookies||{}}else if(d){const l=le({...d,extraParams:c},s,_,I,f);R=l.loginUrl,t=l.cookies||{}}return Object.keys(t).forEach(l=>{T(e,l,t[l].value,t[l].options)}),r.info(`IdP login initiated for ID '${g}'`),e.newResponse(null,302,{Location:R||new URL(e.req.url).pathname})}}function er(n){return async e=>{const r=e.get("logger"),a=await e.req.formData(),o=a.get("SAMLResponse"),I=a.get("RelayState");if(typeof o!="string"||typeof I!="string"){const t="SAMLResponse is required";return r.error(`SAML2 login error: ${t}`),e.text(`Bad request: ${t}`,400)}const i=ue(o),{success:u,uid:s,nameFormat:g,attrs:_,issuerId:m,expiresAt:h}=ge(i),{idpId:C,redirectTo:k}=JSON.parse(I);if(!u){const t="SAML2 assertion is not successful";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!h||Math.ceil(Date.now()/1e3)>=h){const t="SAML2 Token Expired";return r.error(`SAML2 login error: ${t}`),e.text(t,401)}const f=n.getConfig().ssoDirect?.[C];if(!f||!de(f)){const t="Cannot find valid IdP";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!(f.issuerId&&m&&x(f.issuerId,m))){const t="IssuerID is misconfigured or untrusted assertions issuer received";return r.error(`SAML2 login error: ${t}`),e.text(`Permission denied: ${t}`,401)}if(!await me(i,f.x509PublicCert)){const t="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${t}`),e.text(t,401)}const c=pe(s,g,_,f.teamsAttributeName??Q);if(!c.sub){const t="The provider did not return a valid user identity.";return r.error(`SAML2 login error: ${t}`),e.text(t,400)}if(!c.email){const t="The provider did not return a valid user email.";return r.error(`SAML2 login error: ${t}`),e.text(t,400)}let R=await $.sign({...c,idpId:C},U,E.HS256);if(Buffer.byteLength(R)>N){const t=await G({...c},C,"teams",{rbacConfig:n.getConfig().access?.rbac,teamsMapping:f.teamsAttributeMap});if(!t){const l=`The SAML2 claims are too large for the session cookie: ${Buffer.byteLength(JSON.stringify(c))} bytes of claims`;return r.error(`SAML2 login error: ${l}`),e.text(`Forbidden: ${l}`,403)}t.teamsKept<t.teamsTotal&&r.warn(`SAML2 login: the "teams" claim is too large for the session cookie size limit; kept ${t.teamsKept} of ${t.teamsTotal} values, with teams from the RBAC configuration first; RBAC ignores the dropped teams: ${JSON.stringify(t.droppedTeams)}`),R=t.token}return T(e,A.AUTHORIZATION,R,{path:O()||"/",httpOnly:!0,expires:new Date(h*1e3)}),P(e,A.IDP_REFRESH_TOKEN,{path:O()||"/"}),v(e),r.updateContext({email:c.email,subject:c.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:k||"/"})}}function rr(n){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),o=L(H(a.pathname)),I=n.getConfig().ssoDirect,i=Object.entries(I||{}).find(([,f])=>b(f)&&Re(f));if(!(I&&i))return e.newResponse(null,302,{Location:o});const s=e.req.query("code"),g=s?await _e(s):e.req.query("token"),_=g&&await Ae(g);if(!_)return e.newResponse(null,302,{Location:o});if(!ee(_[re]||[]).some(f=>f===oe||f===te))return e.newResponse(null,302,{Location:o});const C=await $.sign({..._,idpId:i?.at(0)},U,E.HS256),k=Date.now()+B*1e3;return T(e,A.AUTHORIZATION,C,{path:O()||"/",httpOnly:!0,expires:new Date(k),sameSite:"None",secure:!0,partitioned:!0}),P(e,A.IDP_REFRESH_TOKEN,{path:O()||"/"}),v(e),r.info("Token login successful"),e.newResponse(null,302,{Location:o})}}export{Ze as authorizeHandler,xe as idpLoginHandler,Qe as inviteHandler,Xe as logoutHandler,We as oidcCallbackHandler,Ye as postLogoutHandler,Ve as redoclyLoginCallbackHandler,rr as redoclyTokenLoginHandler,er as samlCallbackHandler};
|
|
@@ -15,10 +15,10 @@ export type ConsentPageProps = {
|
|
|
15
15
|
clientIdHost: string | undefined;
|
|
16
16
|
redirectUrl: URL;
|
|
17
17
|
scopes: readonly string[];
|
|
18
|
-
|
|
18
|
+
accessLifetimeDays: number;
|
|
19
19
|
rememberDays: number | undefined;
|
|
20
20
|
consentToken: string;
|
|
21
21
|
};
|
|
22
|
-
export declare function consentPage({ site, user, clientName, clientIdHost, redirectUrl, scopes,
|
|
22
|
+
export declare function consentPage({ site, user, clientName, clientIdHost, redirectUrl, scopes, accessLifetimeDays, rememberDays, consentToken, }: ConsentPageProps): string;
|
|
23
23
|
export declare function consentInvalidPage(site: PageSite): string;
|
|
24
24
|
//# sourceMappingURL=mcp-consent-page.d.ts.map
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import{ICONS as d,code as E,escapeHtml as a,htmlDocument as p,siteHeader as N}from"../../../plugins/mcp/handlers/credentials-page/layout.js";import{statusPage as L}from"../../../plugins/mcp/handlers/credentials-page/outcome-pages.js";const $=300,
|
|
2
|
-
<h2 id="${
|
|
1
|
+
import{ICONS as d,code as E,escapeHtml as a,htmlDocument as p,siteHeader as N}from"../../../plugins/mcp/handlers/credentials-page/layout.js";import{statusPage as L}from"../../../plugins/mcp/handlers/credentials-page/outcome-pages.js";const $=300,s={TITLE:"Allow MCP access",INTRO:"An MCP client asks to use this portal with your account.",WARNING:"Allow access only if you started this connection in your MCP client.",REDIRECT_LABEL:"Sends access to",REDIRECT_HINT:"The app receives access at this address.",USER_LABEL:"Signed in as",NOT_PROVIDED:"Not provided",CLIENT_LABEL:"App",CLIENT_UNKNOWN:"The app did not send a name. Continue only if you just started a connection yourself.",CLIENT_NAME_HINT:"The app chose this name itself. We do not check it.",CLIENT_ID_HOST:e=>`App details loaded from ${E(e)}`,PANEL_TITLE:"What the client gets",ACCOUNT_DETAILS:"Your account details, including your name and email address",ACT_AS_YOU:"Use of the MCP server of this portal as you",ACCESS_LIFETIME:e=>`Access that it can renew for up to ${e===1?"1 day":`${e} days`}`,REMEMBER:e=>`If you allow access, we do not ask again for this client for ${e} days.`,SCOPES_LABEL:"Scopes:",ALLOW:"Allow access",DENY:"Deny",INVALID_TITLE:"This request is no longer valid",INVALID_BODY:e=>`An access request works for ${e} minutes. This one has expired or can no longer be used.`,INVALID_NEXT:"Go back to your MCP client and start the connection again."},h={"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","X-Frame-Options":"DENY","Referrer-Policy":"no-referrer","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'; base-uri 'none'"};function _({protocol:e,host:n,pathname:t,search:o}){return`${e}${n?`//${n}`:""}${t}${o}`}function r(e,n,t){return`<section aria-labelledby="${e}">
|
|
2
|
+
<h2 id="${e}">${a(n)}</h2>
|
|
3
3
|
${t.filter(Boolean).join(`
|
|
4
4
|
`)}
|
|
5
|
-
</section>`}function m(
|
|
6
|
-
<li>${
|
|
7
|
-
<li>${
|
|
8
|
-
<li>${
|
|
9
|
-
</ol>`,c=t?`<p>${
|
|
5
|
+
</section>`}function m(e,n){return r("consent-client",s.CLIENT_LABEL,[e?`<p>${a(e)}</p><p class="muted small">${s.CLIENT_NAME_HINT}</p>`:`<p>${s.CLIENT_UNKNOWN}</p>`,n?`<p class="muted small">${s.CLIENT_ID_HOST(n)}</p>`:""])}function A({scopes:e,accessLifetimeDays:n,rememberDays:t}){const o=`${d.shield}${s.PANEL_TITLE}`,i=`<ol class="steps">
|
|
6
|
+
<li>${s.ACCOUNT_DETAILS}</li>
|
|
7
|
+
<li>${s.ACT_AS_YOU}</li>
|
|
8
|
+
<li>${s.ACCESS_LIFETIME(n)}</li>
|
|
9
|
+
</ol>`,c=t?`<p>${s.REMEMBER(t)}</p>`:"";return`<aside class="panel" aria-labelledby="panel-title">
|
|
10
10
|
<div class="panel-steps"><h2 id="panel-title">${o}</h2>${i}</div>
|
|
11
11
|
<details class="panel-steps-toggle"><summary><h2>${o}</h2></summary>${i}</details>
|
|
12
12
|
<div class="link-facts">
|
|
13
13
|
${c}
|
|
14
|
-
<div class="tags"><span>${
|
|
14
|
+
<div class="tags"><span>${s.SCOPES_LABEL}</span>${e.map(l=>`<span class="tag">${a(l)}</span>`).join("")}</div>
|
|
15
15
|
</div>
|
|
16
|
-
</aside>`}function S({site:
|
|
16
|
+
</aside>`}function S({site:e,user:n,clientName:t,clientIdHost:o,redirectUrl:i,scopes:c,accessLifetimeDays:l,rememberDays:T,consentToken:u}){const I=`<div class="page">
|
|
17
17
|
<main class="card">
|
|
18
|
-
${N(
|
|
19
|
-
<h1>${
|
|
20
|
-
<p class="lede">${
|
|
21
|
-
<div class="banner warning" role="note">${d.warning}<p>${
|
|
18
|
+
${N(e)}
|
|
19
|
+
<h1>${s.TITLE}</h1>
|
|
20
|
+
<p class="lede">${s.INTRO}</p>
|
|
21
|
+
<div class="banner warning" role="note">${d.warning}<p>${s.WARNING}</p></div>
|
|
22
22
|
<div class="info-card">
|
|
23
23
|
${m(t,o)}
|
|
24
|
-
${r("consent-user",
|
|
25
|
-
${r("consent-redirect",
|
|
24
|
+
${r("consent-user",s.USER_LABEL,[`<p>${a(n||s.NOT_PROVIDED)}</p>`])}
|
|
25
|
+
${r("consent-redirect",s.REDIRECT_LABEL,[`<p>${E(_(i))}</p>`,`<p class="muted small">${s.REDIRECT_HINT}</p>`])}
|
|
26
26
|
</div>
|
|
27
27
|
<form class="credentials" method="post" autocomplete="off">
|
|
28
28
|
<input type="hidden" name="consent" value="${a(u)}">
|
|
29
29
|
<div class="actions">
|
|
30
|
-
<div class="actions-row"><button class="secondary" type="submit" name="decision" value="deny">${
|
|
30
|
+
<div class="actions-row"><button class="secondary" type="submit" name="decision" value="deny">${s.DENY}</button><button class="primary" type="submit" name="decision" value="allow">${s.ALLOW}</button></div>
|
|
31
31
|
</div>
|
|
32
32
|
</form>
|
|
33
33
|
</main>
|
|
34
|
-
${A({scopes:c,
|
|
35
|
-
</div>`;return p({title:
|
|
34
|
+
${A({scopes:c,accessLifetimeDays:l,rememberDays:T})}
|
|
35
|
+
</div>`;return p({title:s.TITLE,body:I},e.colorScheme)}function y(e){return p(L("warning",s.INVALID_TITLE,[s.INVALID_BODY($/60),s.INVALID_NEXT]),e.colorScheme)}export{h as CONSENT_PAGE_HEADERS,$ as MCP_CONSENT_TTL_SEC,y as consentInvalidPage,S as consentPage};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getCookie as H}from"hono/cookie";import{createHash as G,timingSafeEqual as oe}from"node:crypto";import{ulid as ie}from"ulid";import{isRedoclySso as se,isOidcProviderConfig as R,createMcpAuthorizationCode as ae,verifyMcpAuthorizationCode as de,createMcpRefreshToken as K,verifyMcpRefreshToken as ce,createMcpSessionResource as M,oidcRefreshTokenGrant as pe,getOidcMetadata as ue,getUserParamsFromCookies as le,idTokenIsForThisProject as _e}from"../../auth.js";import{isMcpContentProtected as P}from"../../../plugins/mcp/auth/auth-handlers.js";import{resolveSiteBranding as fe}from"../../../plugins/mcp/handlers/credentials-page/site-branding.js";import{SUPPORTED_MCP_SCOPES as E}from"../../../plugins/mcp/constants.js";import{AUTH_URL as he,JWT_SECRET_KEY as m,MCP_ACCESS_TOKEN_TTL_SEC as b,MCP_REFRESH_SESSION_LIFETIME_SEC as B}from"../../../constants/common.js";import{ServerRoutes as w}from"../../../../constants/api.js";import{AuthCookieNames as v}from"../../../../constants/common.js";import{withPathPrefix as I}from"@redocly/theme/core/utils";import{telemetry as y}from"../../../telemetry/index.js";import{envConfig as q}from"../../../config/env-config.js";import{hasProjectDatabase as j}from"../../../providers/database/has-project-database.js";import*as C from"../../jwt/jwt.js";import{InternalTokenTypes as O,McpAccessTokenMarker as U}from"../../jwt/token-types.js";import{AlgorithmTypes as A,JwtAlgorithmNotImplemented as me,JwtTokenExpired as ge,JwtTokenInvalid as Ce,JwtTokenNotBefore as Te,JwtTokenSignatureMismatched as ke}from"../../jwt/types.js";import{getRequestOrigin as L}from"../../utils/get-request-origin.js";import{encryptClaimValue as J}from"../../../utils/crypto/encrypt-claim-value.js";import{decryptAesGcm as Se}from"../../../utils/crypto/decrypt-aes-gcm.js";import{encryptAesGcm as V}from"../../../utils/crypto/encrypt-aes-gcm.js";import{fetchClientMetadataDocument as we,isClientMetadataDocumentUrl as z,isLoopbackRedirect as W,isRedirectUriRegistered as Ie}from"./mcp-client-metadata.js";import{CONSENT_PAGE_HEADERS as Y,MCP_CONSENT_TTL_SEC as Me,consentInvalidPage as Ee,consentPage as ye}from"./mcp-consent-page.js";import{MCP_CONSENT_MEMORY_DAYS as Ae,hasConsent as Re,redeemAuthorizationCode as Pe,redeemRefreshToken as ve,saveConsent as Oe}from"./mcp-oauth-store.js";const s=(r,e,t=200,n)=>r.json(e,t,{"Content-Type":"application/json",...n??{}}),$=new Set(["S256"]),F=["authorization_code","refresh_token"];function Ue(r,e,t){if(!r||!$.has(e||""))return!1;const n=G("sha256").update(t).digest("base64url"),i=Buffer.from(n),a=Buffer.from(r);return i.length!==a.length?!1:oe(i,a)}async function Le(r){const e=Math.floor(Date.now()/1e3);return C.sign({type:O.MCP_CONTEXT,...r,iat:e,exp:e+600},m,A.HS256)}async function Ne(r){await C.verify(r,m,A.HS256);const{payload:e}=C.decode(r);if(e.type!==O.MCP_CONTEXT)throw new Error("Invalid context token type");return e}function fr(r){return async e=>{if(!P(r))return s(e,{error:"not_found"},404);if(e.req.method!=="GET")return s(e,{error:"Method not allowed"},405,{Allow:"GET"});const t=L(e);return s(e,{resource:`${t}${I("/mcp")}`,authorization_servers:[t],bearer_methods_supported:["header"],resource_documentation:`${t}${w.MCP_OAUTH_AUTHORIZATION_SERVER}`,scopes_supported:[...E],bearer_token_types_supported:["Bearer"]},200,{"Access-Control-Allow-Origin":"*"})}}function hr(r){return async e=>{if(!P(r))return s(e,{error:"not_found"},404);const t=L(e);return s(e,{issuer:t,authorization_endpoint:`${t}${I(w.MCP_AUTHORIZATION)}`,token_endpoint:`${t}${I(w.MCP_TOKEN_PORTAL)}`,scopes_supported:[...E],registration_endpoint:`${t}${I(w.MCP_DYNAMIC_CLIENT_REGISTRATION)}`,response_types_supported:["code"],grant_types_supported:[...F],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],code_challenge_methods_supported:["S256"],token_endpoint_auth_methods_supported:["none"],client_id_metadata_document_supported:!0},200,{"Access-Control-Allow-Origin":"*"})}}function mr(r){return async e=>{if(!P(r))return s(e,{error:"not_found"},404);if(e.req.method!=="POST")return s(e,{error:"Method not allowed"},405);try{return s(e,{client_id:q.OAUTH_CLIENT_ID||"",client_name:"MCP Client",redirect_uris:[],grant_types:[...F],response_types:["code"],scope:E.join(" "),subject_type:"public",token_endpoint_auth_method:"none",created_at:new Date().toISOString(),updated_at:new Date().toISOString()},201,{"Access-Control-Allow-Origin":"*"})}catch(t){return s(e,{error:"invalid_request",error_description:t?.message||"Unable to register client"},500,{"Access-Control-Allow-Origin":"*"})}}}const De=new Set(["http:","https:","ws:","wss:","ftp:","file:"]),He=new Set(["javascript:","data:","blob:","about:","vbscript:","intent:","chrome:","view-source:","ms-msdt:","ms-appx-web:"]),be=2048,qe=/[\s\p{Cc}]/u;function Z(r){if(!r||r.length>be||r.includes("#")||qe.test(r))return;const e=URL.parse(r);if(e){if(e.protocol==="https:"||W(e))return r.toLowerCase().startsWith(`${e.protocol}//`)?e:void 0;if(!(De.has(e.protocol)||He.has(e.protocol)))return e}}function X(r){const e=r||{},t=Object.keys(e),n=t.find(a=>R(e[a])&&se(e[a])),i=t.find(a=>R(e[a]));return n||i||t[0]}function gr(r){return async e=>{if(!P(r))return s(e,{error:"not_found"},404);const t=new URL(e.req.url),{searchParams:n}=t,i=n.get("redirect_uri"),a=n.get("client_id"),d=n.get("code_challenge"),o=n.get("code_challenge_method"),c=ie();if(!i||!Z(i))return s(e,{error:"invalid_request",error_description:"redirect_uri must be an https URL, a loopback http URL, or a private-use scheme"},400);if(!d)return s(e,{error:"invalid_request",error_description:"code_challenge is required"},400);if(!$.has(o||""))return s(e,{error:"invalid_request",error_description:`Unsupported code_challenge_method. Supported: ${[...$].join(", ")}`},400);let p;if(z(a))try{const u=await we(a);if(p=u.client_name,!Ie(i,u.redirect_uris))return s(e,{error:"invalid_request",error_description:"redirect_uri is not registered for this client"},400)}catch(u){const f=u instanceof Error?u.message:String(u);return s(e,{error:"invalid_client",error_description:`Unable to resolve client metadata document: ${f}`},400)}y.sendMcpAuthorizationStartedMessage([{...M(c),redirect_uri:i||null}]);const l=L(e),k={isMcpFlow:!0,originalRedirectUri:i,mcpClientId:a,mcpState:n.get("state"),mcpSessionId:c,mcpCodeChallenge:d,mcpCodeChallengeMethod:o,mcpClientName:p,timestamp:Date.now()};try{const u=await Le(k),f=new URL(I(w.IDP_LOGIN),l);f.searchParams.set("redirectTo",`${w.MCP_CALLBACK}/${u}`);const h=X(r.getConfig().ssoDirect);return h?(f.searchParams.set("idpId",h),e.redirect(f.toString())):s(e,{error:"not_found"},404)}catch(u){const f=u instanceof Error?u.message:String(u),h=u instanceof Error?u.stack:String(u);y.sendMcpAuthorizationFailedMessage([{...M(c),error:f,error_details:h}]);const S=new URL(I(`${he}/oauth2/auth`));return S.search=n.toString(),e.redirect(S.toString())}}}async function Q(r){const e=Math.floor(Date.now()/1e3),t=r.ttlSec?e+r.ttlSec:Math.min(typeof r.claims.exp=="number"?r.claims.exp:1/0,e+b);return{accessToken:await C.sign({...r.claims,[U.CLAIM]:U.VALUE,...r.idpId?{idpId:r.idpId}:{},...r.idpAccessTokenEnc?{idp_access_token:r.idpAccessTokenEnc}:{},...r.ttlSec?{iat:e}:{},exp:t},m,A.HS256,r.kid),expiresIn:Math.max(t-e,0)}}async function je(r,e,t){const n=T=>s(r,{error:"invalid_grant",error_description:T},400),i=r.get("logger"),a=t.get("refresh_token"),d=t.get("client_id")||void 0;if(!a)return s(r,{error:"invalid_request",error_description:"refresh_token is required"},400);let o;try{o=await ce(a)}catch(T){return i?.warn("MCP refresh token rejected: %s",T instanceof Error?T.message:String(T)),n("Invalid refresh token")}if(d&&d!==o.client_id)return n("Client mismatch");const c=o.idp_id?e.getConfig().ssoDirect?.[o.idp_id]:void 0;let p,l,k=o.idp_access_token_enc,u=o.idp_refresh_token_enc;if(o.idp_refresh_token_enc){let T;try{T=Se(o.idp_refresh_token_enc,m)}catch{return n("Invalid refresh token")}if(!o.idp_id||!R(c))return n("Unknown identity provider");let _;try{const g=await ue(o.idp_id,c);if(!g?.token_endpoint)return n("Identity provider has no token endpoint");_=await pe(g.token_endpoint,T,c)}catch(g){return i?.warn("MCP refresh failed to reach the IdP token endpoint: %s",g instanceof Error?g.message:String(g)),s(r,{error:"server_error",error_description:"Failed to refresh session"},500)}if(_.error||!(_.id_token||_.access_token))return i?.warn("MCP refresh rejected by IdP: %s",_.error||"empty token response"),n("Refresh rejected by identity provider");if(_.refresh_token&&(u=V(_.refresh_token,m)),_.access_token&&(k=J(_.access_token,m)),_.id_token){try{const g=C.decode(_.id_token);p=g.payload,l=g.header.kid}catch{return n("Invalid token response from identity provider")}if(p.iss!==o.claims.iss||p.sub!==o.claims.sub)return i?.warn("MCP refresh rejected: the id_token names another issuer or subject"),n("Invalid refresh token")}}const f=Math.min(R(c)&&c.tokenExpirationTime||b,b),h=p??o.claims;if(!_e(h,c))return i?.warn("MCP refresh rejected: the id_token was issued for another project"),n("Invalid refresh token");if(j()&&!await ve(await e.getKv(),a,o.exp))return i?.warn("MCP refresh rejected: the refresh token was already used"),n("Refresh token already used");const S=await Q({claims:h,kid:l,idpId:o.idp_id,idpAccessTokenEnc:k,ttlSec:p?void 0:f}),D=await K({clientId:o.client_id,idpId:o.idp_id,claims:h,idpAccessTokenEnc:k,idpRefreshTokenEnc:u,sessionExp:o.session_exp});return s(r,{access_token:S.accessToken,token_type:"Bearer",expires_in:S.expiresIn,refresh_token:D,scope:E.join(" ")},200,{"Cache-Control":"no-store",Pragma:"no-cache"})}function Cr(r){return async e=>{if(e.req.method!=="POST")return s(e,{error:"Method not allowed"},405);try{const t=await e.req.formData(),n=t.get("grant_type"),i=t.get("code"),a=t.get("redirect_uri")||void 0,d=t.get("code_verifier")||void 0;if(!F.includes(n))return s(e,{error:"invalid_request",error_description:"Invalid grant type"},400);if(n==="refresh_token")return await je(e,r,t);if(!i)return s(e,{error:"invalid_request",error_description:"Missing authorization code"},400);try{const o=await de(i);if(a&&a!==o.redirect_uri)return s(e,{error:"invalid_grant",error_description:"redirect_uri mismatch"},400);if(!d)return s(e,{error:"invalid_grant",error_description:"code_verifier required"},400);if(!Ue(o.code_challenge,o.code_challenge_method,d))return s(e,{error:"invalid_grant",error_description:"code_verifier mismatch"},400);if(q.OAUTH_CLIENT_ID&&o.client_id&&!z(o.client_id)&&o.client_id!==q.OAUTH_CLIENT_ID)return s(e,{error:"invalid_client",error_description:"Client mismatch"},400);const c=o.id_token;if(typeof c!="string"||c.length===0)return s(e,{error:"invalid_grant",error_description:"Missing id_token in authorization code"},400);const p=await x(c,r.getConfig().ssoDirect);if(!p)return s(e,{error:"invalid_grant",error_description:"Invalid authorization code"},400);if(j()&&!await Pe(await r.getKv(),i,o.exp))return s(e,{error:"invalid_grant",error_description:"Authorization code already used"},400);const l=o.idp_access_token_enc,{accessToken:k,expiresIn:u}=await Q({claims:p.payload,kid:p.kid,idpId:o.idp_id,idpAccessTokenEnc:l}),h=Math.floor(Date.now()/1e3)+B,S=await K({clientId:o.client_id,idpId:o.idp_id,claims:p.payload,idpAccessTokenEnc:l,idpRefreshTokenEnc:o.idp_refresh_token_enc,sessionExp:h}),D={access_token:k,token_type:"Bearer",expires_in:u,refresh_token:S,scope:E.join(" ")};return s(e,D,200,{"Cache-Control":"no-store",Pragma:"no-cache"})}catch(o){return e.get("logger")?.warn("MCP token exchange failed: %s",o instanceof Error?o.message:String(o)),s(e,{error:"invalid_grant",error_description:"Invalid authorization code"},400)}}catch(t){const n=t instanceof Error?t.message:String(t);return s(e,{error:"server_error",error_description:"Failed to process token request",error_details:n},500)}}}async function x(r,e){const t=await le(e||{},{[v.AUTHORIZATION]:r});if(t.isAuthenticated!==!0)return;const{payload:n,header:i}=C.decode(r);if(n[U.CLAIM]!==U.VALUE)return{payload:n,kid:i.kid,idpId:typeof t.idpId=="string"?t.idpId:void 0}}function N(r){return G("sha256").update(r).digest("base64url")}async function ze(r){const e=Math.floor(Date.now()/1e3);return C.sign({type:O.MCP_CONSENT,session_hash:N(r.sessionToken),context_hash:N(r.contextToken),iat:e,exp:e+Me},m,A.HS256)}async function $e(r,e){try{await C.verify(r,m,A.HS256)}catch{return!1}const{payload:t}=C.decode(r);return t.type===O.MCP_CONSENT&&t.session_hash===N(e.sessionToken)&&t.context_hash===N(e.contextToken)}function ee(r,e){return async t=>{const n=new URL(t.req.url);let i=n.searchParams.get("context");if(!i&&n.pathname.startsWith(I(`${w.MCP_CALLBACK}/`))){const d=n.pathname.split("/");i=d[d.length-1]}if(!i)return t.text("Missing context parameter",400);let a=null;try{const d=await Ne(i);a=d.mcpSessionId||null;const o=d.originalRedirectUri,c=Z(o);if(!d.isMcpFlow||!o||!c)throw new Error("Invalid MCP context");const p=H(t,v.AUTHORIZATION),l=p?await x(p,r.getConfig().ssoDirect):void 0;return!p||!l?(y.sendMcpAuthorizationFailedMessage([{...M(a),error:"Authentication required",error_details:null}]),t.text("Authentication required",401)):await e(t,{contextToken:i,mcpContext:d,redirectUri:o,redirectUrl:c,sessionToken:p,session:l})}catch(d){const o=Ke(d);return y.sendMcpAuthorizationFailedMessage([{...M(a),...o}]),t.text(`Invalid MCP callback: ${o.error}`,400)}}}async function re(r,e,{mcpContext:t,redirectUri:n,redirectUrl:i,sessionToken:a,session:d}){const o=H(r,v.IDP_ACCESS_TOKEN),c=H(r,v.IDP_REFRESH_TOKEN),p=await ae({idToken:a,idpId:d.idpId??X(e.getConfig().ssoDirect),idpAccessTokenEnc:o?J(o,m):void 0,idpRefreshTokenEnc:c?V(c,m):void 0,clientId:t.mcpClientId||"",redirectUri:n,codeChallenge:t.mcpCodeChallenge||void 0,codeChallengeMethod:t.mcpCodeChallengeMethod||void 0,ttlSec:600}),l=new URL(i);return l.searchParams.set("code",p),t.mcpState&&l.searchParams.set("state",t.mcpState),y.sendMcpAuthorizationCompletedMessage([{...M(t.mcpSessionId||null),redirect_uri:n}]),r.redirect(l.toString())}function Fe(r,{mcpContext:e,redirectUrl:t}){const n=new URL(t);return n.searchParams.set("error","access_denied"),e.mcpState&&n.searchParams.set("state",e.mcpState),y.sendMcpAuthorizationFailedMessage([{...M(e.mcpSessionId||null),error:"access_denied",error_details:null}]),r.redirect(n.toString())}function Ge({payload:r}){return[r.email,r.name,r.sub].find(e=>typeof e=="string"&&e.length>0)}function te({mcpContext:r,redirectUri:e,session:t}){const{sub:n}=t.payload;if(!(!j()||typeof n!="string"||n.length===0))return{idpId:t.idpId,subject:n,clientId:r.mcpClientId||"",redirectUri:e}}function ne(r,e){const{seo:t,colorMode:n}=e.getConfig(),{logo:i}=e.getGlobalData();return{...fe({seo:t,logo:i,colorMode:n}),host:new URL(L(r)).host}}function Tr(r){return ee(r,async(e,t)=>{const n=te(t);if(W(t.redirectUrl)||n&&await Re(await r.getKv(),n))return re(e,r,t);const{mcpClientId:i,mcpClientName:a}=t.mcpContext,d=ye({site:ne(e,r),user:Ge(t.session),clientName:a||void 0,clientIdHost:z(i)?new URL(i).host:void 0,redirectUrl:t.redirectUrl,scopes:E,accessLifetimeHours:B/3600,rememberDays:n?Ae:void 0,consentToken:await ze(t)});return new Response(d,{status:200,headers:Y})})}function kr(r){return ee(r,async(e,t)=>{const n=await e.req.formData(),i=n.get("consent");if(typeof i!="string"||!await $e(i,t))return new Response(Ee(ne(e,r)),{status:403,headers:Y});if(n.get("decision")==="allow"){const a=te(t);return a&&await Oe(await r.getKv(),a),re(e,r,t)}return Fe(e,t)})}function Ke(r){return r instanceof Error&&[me,ge,Ce,Te,ke].some(e=>r instanceof e)?{error:r.name,error_details:null}:r instanceof Error?{error:r.message,error_details:r.stack??null}:{error:String(r),error_details:null}}export{F as SUPPORTED_MCP_GRANT_TYPES,Le as createMcpContextToken,gr as mcpAuthorizationHandler,Tr as mcpCallbackHandler,kr as mcpConsentHandler,mr as mcpDynamicClientRegistrationHandler,hr as mcpOAuthAuthorizationServerHandler,fr as mcpOAuthProtectedResourceHandler,Cr as mcpTokenPortalHandler,X as resolveMcpIdpId,Ne as verifyAndParseMcpContextToken,Ue as verifyPkce};
|
|
1
|
+
import{getCookie as v}from"hono/cookie";import{createHash as G,timingSafeEqual as se}from"node:crypto";import{ulid as ae}from"ulid";import{isRedoclySso as ce,isOidcProviderConfig as O,createMcpAuthorizationCode as de,verifyMcpAuthorizationCode as pe,createMcpRefreshToken as B,verifyMcpRefreshToken as ue,createMcpSessionResource as M,oidcRefreshTokenGrant as le,getOidcMetadata as _e,getUserParamsFromCookies as fe,idTokenIsForThisProject as he}from"../../auth.js";import{isMcpContentProtected as U}from"../../../plugins/mcp/auth/auth-handlers.js";import{resolveSiteBranding as me}from"../../../plugins/mcp/handlers/credentials-page/site-branding.js";import{SUPPORTED_MCP_SCOPES as E}from"../../../plugins/mcp/constants.js";import{AUTH_URL as ge,JWT_SECRET_KEY as m,MCP_ACCESS_TOKEN_TTL_SEC as b,MCP_CLAIMS_ONLY_SESSION_LIFETIME_SEC as K,MCP_REFRESH_SESSION_LIFETIME_SEC as Ce}from"../../../constants/common.js";import{ServerRoutes as I}from"../../../../constants/api.js";import{AuthCookieNames as A}from"../../../../constants/common.js";import{withPathPrefix as w}from"@redocly/theme/core/utils";import{telemetry as y}from"../../../telemetry/index.js";import{envConfig as q}from"../../../config/env-config.js";import{hasProjectDatabase as j}from"../../../providers/database/has-project-database.js";import*as C from"../../jwt/jwt.js";import{InternalTokenTypes as L,McpAccessTokenMarker as N}from"../../jwt/token-types.js";import{AlgorithmTypes as R,JwtAlgorithmNotImplemented as Te,JwtTokenExpired as ke,JwtTokenInvalid as Se,JwtTokenNotBefore as Ie,JwtTokenSignatureMismatched as we}from"../../jwt/types.js";import{getRequestOrigin as D}from"../../utils/get-request-origin.js";import{encryptClaimValue as J}from"../../../utils/crypto/encrypt-claim-value.js";import{decryptAesGcm as Me}from"../../../utils/crypto/decrypt-aes-gcm.js";import{encryptAesGcm as V}from"../../../utils/crypto/encrypt-aes-gcm.js";import{fetchClientMetadataDocument as Ee,isClientMetadataDocumentUrl as z,isLoopbackRedirect as W,isRedirectUriRegistered as ye}from"./mcp-client-metadata.js";import{CONSENT_PAGE_HEADERS as Y,MCP_CONSENT_TTL_SEC as Ae,consentInvalidPage as Re,consentPage as Pe}from"./mcp-consent-page.js";import{MCP_CONSENT_MEMORY_DAYS as ve,hasConsent as Oe,redeemAuthorizationCode as Ue,redeemRefreshToken as Le,saveConsent as Ne}from"./mcp-oauth-store.js";const s=(r,e,t=200,n)=>r.json(e,t,{"Content-Type":"application/json",...n??{}}),$=new Set(["S256"]),F=["authorization_code","refresh_token"];function De(r,e,t){if(!r||!$.has(e||""))return!1;const n=G("sha256").update(t).digest("base64url"),i=Buffer.from(n),a=Buffer.from(r);return i.length!==a.length?!1:se(i,a)}async function He(r){const e=Math.floor(Date.now()/1e3);return C.sign({type:L.MCP_CONTEXT,...r,iat:e,exp:e+600},m,R.HS256)}async function be(r){await C.verify(r,m,R.HS256);const{payload:e}=C.decode(r);if(e.type!==L.MCP_CONTEXT)throw new Error("Invalid context token type");return e}function gr(r){return async e=>{if(!U(r))return s(e,{error:"not_found"},404);if(e.req.method!=="GET")return s(e,{error:"Method not allowed"},405,{Allow:"GET"});const t=D(e);return s(e,{resource:`${t}${w("/mcp")}`,authorization_servers:[t],bearer_methods_supported:["header"],resource_documentation:`${t}${I.MCP_OAUTH_AUTHORIZATION_SERVER}`,scopes_supported:[...E],bearer_token_types_supported:["Bearer"]},200,{"Access-Control-Allow-Origin":"*"})}}function Cr(r){return async e=>{if(!U(r))return s(e,{error:"not_found"},404);const t=D(e);return s(e,{issuer:t,authorization_endpoint:`${t}${w(I.MCP_AUTHORIZATION)}`,token_endpoint:`${t}${w(I.MCP_TOKEN_PORTAL)}`,scopes_supported:[...E],registration_endpoint:`${t}${w(I.MCP_DYNAMIC_CLIENT_REGISTRATION)}`,response_types_supported:["code"],grant_types_supported:[...F],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],code_challenge_methods_supported:["S256"],token_endpoint_auth_methods_supported:["none"],client_id_metadata_document_supported:!0},200,{"Access-Control-Allow-Origin":"*"})}}function Tr(r){return async e=>{if(!U(r))return s(e,{error:"not_found"},404);if(e.req.method!=="POST")return s(e,{error:"Method not allowed"},405);try{return s(e,{client_id:q.OAUTH_CLIENT_ID||"",client_name:"MCP Client",redirect_uris:[],grant_types:[...F],response_types:["code"],scope:E.join(" "),subject_type:"public",token_endpoint_auth_method:"none",created_at:new Date().toISOString(),updated_at:new Date().toISOString()},201,{"Access-Control-Allow-Origin":"*"})}catch(t){return s(e,{error:"invalid_request",error_description:t?.message||"Unable to register client"},500,{"Access-Control-Allow-Origin":"*"})}}}const qe=new Set(["http:","https:","ws:","wss:","ftp:","file:"]),je=new Set(["javascript:","data:","blob:","about:","vbscript:","intent:","chrome:","view-source:","ms-msdt:","ms-appx-web:"]),ze=2048,$e=/[\s\p{Cc}]/u;function Z(r){return r?Ce:K}function X(r){if(!r||r.length>ze||r.includes("#")||$e.test(r))return;const e=URL.parse(r);if(e){if(e.protocol==="https:"||W(e))return r.toLowerCase().startsWith(`${e.protocol}//`)?e:void 0;if(!(qe.has(e.protocol)||je.has(e.protocol)))return e}}function Q(r){const e=r||{},t=Object.keys(e),n=t.find(a=>O(e[a])&&ce(e[a])),i=t.find(a=>O(e[a]));return n||i||t[0]}function kr(r){return async e=>{if(!U(r))return s(e,{error:"not_found"},404);const t=new URL(e.req.url),{searchParams:n}=t,i=n.get("redirect_uri"),a=n.get("client_id"),c=n.get("code_challenge"),o=n.get("code_challenge_method"),p=ae();if(!i||!X(i))return s(e,{error:"invalid_request",error_description:"redirect_uri must be an https URL, a loopback http URL, or a private-use scheme"},400);if(!c)return s(e,{error:"invalid_request",error_description:"code_challenge is required"},400);if(!$.has(o||""))return s(e,{error:"invalid_request",error_description:`Unsupported code_challenge_method. Supported: ${[...$].join(", ")}`},400);let d;if(z(a))try{const u=await Ee(a);if(d=u.client_name,!ye(i,u.redirect_uris))return s(e,{error:"invalid_request",error_description:"redirect_uri is not registered for this client"},400)}catch(u){const h=u instanceof Error?u.message:String(u);return s(e,{error:"invalid_client",error_description:`Unable to resolve client metadata document: ${h}`},400)}y.sendMcpAuthorizationStartedMessage([{...M(p),redirect_uri:i||null}]);const l=D(e),k={isMcpFlow:!0,originalRedirectUri:i,mcpClientId:a,mcpState:n.get("state"),mcpSessionId:p,mcpCodeChallenge:c,mcpCodeChallengeMethod:o,mcpClientName:d,timestamp:Date.now()};try{const u=await He(k),h=new URL(w(I.IDP_LOGIN),l);h.searchParams.set("redirectTo",`${I.MCP_CALLBACK}/${u}`);const _=Q(r.getConfig().ssoDirect);return _?(h.searchParams.set("idpId",_),e.redirect(h.toString())):s(e,{error:"not_found"},404)}catch(u){const h=u instanceof Error?u.message:String(u),_=u instanceof Error?u.stack:String(u);y.sendMcpAuthorizationFailedMessage([{...M(p),error:h,error_details:_}]);const S=new URL(w(`${ge}/oauth2/auth`));return S.search=n.toString(),e.redirect(S.toString())}}}async function x(r){const e=Math.floor(Date.now()/1e3),t=r.ttlSec?e+r.ttlSec:Math.min(typeof r.claims.exp=="number"?r.claims.exp:1/0,e+b);return{accessToken:await C.sign({...r.claims,[N.CLAIM]:N.VALUE,...r.idpId?{idpId:r.idpId}:{},...r.idpAccessTokenEnc?{idp_access_token:r.idpAccessTokenEnc}:{},...r.ttlSec?{iat:e}:{},exp:t},m,R.HS256,r.kid),expiresIn:Math.max(t-e,0)}}async function Fe(r,e,t){const n=T=>s(r,{error:"invalid_grant",error_description:T},400),i=r.get("logger"),a=t.get("refresh_token"),c=t.get("client_id")||void 0;if(!a)return s(r,{error:"invalid_request",error_description:"refresh_token is required"},400);let o;try{o=await ue(a)}catch(T){return i?.warn("MCP refresh token rejected: %s",T instanceof Error?T.message:String(T)),n("Invalid refresh token")}if(c&&c!==o.client_id)return n("Client mismatch");const p=o.idp_id?e.getConfig().ssoDirect?.[o.idp_id]:void 0;let d,l,k=o.idp_access_token_enc,u=o.idp_refresh_token_enc;if(o.idp_refresh_token_enc){let T;try{T=Me(o.idp_refresh_token_enc,m)}catch{return n("Invalid refresh token")}if(!o.idp_id||!O(p))return n("Unknown identity provider");let f;try{const g=await _e(o.idp_id,p);if(!g?.token_endpoint)return n("Identity provider has no token endpoint");f=await le(g.token_endpoint,T,p)}catch(g){return i?.warn("MCP refresh failed to reach the IdP token endpoint: %s",g instanceof Error?g.message:String(g)),s(r,{error:"server_error",error_description:"Failed to refresh session"},500)}if(f.error||!(f.id_token||f.access_token))return i?.warn("MCP refresh rejected by IdP: %s",f.error||"empty token response"),n("Refresh rejected by identity provider");if(f.refresh_token&&(u=V(f.refresh_token,m)),f.access_token&&(k=J(f.access_token,m)),f.id_token){try{const g=C.decode(f.id_token);d=g.payload,l=g.header.kid}catch{return n("Invalid token response from identity provider")}if(d.iss!==o.claims.iss||d.sub!==o.claims.sub)return i?.warn("MCP refresh rejected: the id_token names another issuer or subject"),n("Invalid refresh token")}}const h=Math.min(O(p)&&p.tokenExpirationTime||b,b),_=d??o.claims;if(!he(_,p))return i?.warn("MCP refresh rejected: the id_token was issued for another project"),n("Invalid refresh token");const S=d||typeof _.iat!="number"?o.session_exp:Math.min(o.session_exp,_.iat+K);if(S<=Math.floor(Date.now()/1e3))return i?.warn("MCP refresh rejected: the claims snapshot is older than the claims-only window"),n("Refresh token expired");if(j()&&!await Le(await e.getKv(),a,o.exp))return i?.warn("MCP refresh rejected: the refresh token was already used"),n("Refresh token already used");const P=await x({claims:_,kid:l,idpId:o.idp_id,idpAccessTokenEnc:k,ttlSec:d?void 0:h}),ie=await B({clientId:o.client_id,idpId:o.idp_id,claims:_,idpAccessTokenEnc:k,idpRefreshTokenEnc:u,sessionExp:S});return s(r,{access_token:P.accessToken,token_type:"Bearer",expires_in:P.expiresIn,refresh_token:ie,scope:E.join(" ")},200,{"Cache-Control":"no-store",Pragma:"no-cache"})}function Sr(r){return async e=>{if(e.req.method!=="POST")return s(e,{error:"Method not allowed"},405);try{const t=await e.req.formData(),n=t.get("grant_type"),i=t.get("code"),a=t.get("redirect_uri")||void 0,c=t.get("code_verifier")||void 0;if(!F.includes(n))return s(e,{error:"invalid_request",error_description:"Invalid grant type"},400);if(n==="refresh_token")return await Fe(e,r,t);if(!i)return s(e,{error:"invalid_request",error_description:"Missing authorization code"},400);try{const o=await pe(i);if(a&&a!==o.redirect_uri)return s(e,{error:"invalid_grant",error_description:"redirect_uri mismatch"},400);if(!c)return s(e,{error:"invalid_grant",error_description:"code_verifier required"},400);if(!De(o.code_challenge,o.code_challenge_method,c))return s(e,{error:"invalid_grant",error_description:"code_verifier mismatch"},400);if(q.OAUTH_CLIENT_ID&&o.client_id&&!z(o.client_id)&&o.client_id!==q.OAUTH_CLIENT_ID)return s(e,{error:"invalid_client",error_description:"Client mismatch"},400);const p=o.id_token;if(typeof p!="string"||p.length===0)return s(e,{error:"invalid_grant",error_description:"Missing id_token in authorization code"},400);const d=await ee(p,r.getConfig().ssoDirect);if(!d)return s(e,{error:"invalid_grant",error_description:"Invalid authorization code"},400);if(j()&&!await Ue(await r.getKv(),i,o.exp))return s(e,{error:"invalid_grant",error_description:"Authorization code already used"},400);const l=o.idp_access_token_enc,{accessToken:k,expiresIn:u}=await x({claims:d.payload,kid:d.kid,idpId:o.idp_id,idpAccessTokenEnc:l}),_=Math.floor(Date.now()/1e3)+Z(!!o.idp_refresh_token_enc),S=await B({clientId:o.client_id,idpId:o.idp_id,claims:d.payload,idpAccessTokenEnc:l,idpRefreshTokenEnc:o.idp_refresh_token_enc,sessionExp:_}),P={access_token:k,token_type:"Bearer",expires_in:u,refresh_token:S,scope:E.join(" ")};return s(e,P,200,{"Cache-Control":"no-store",Pragma:"no-cache"})}catch(o){return e.get("logger")?.warn("MCP token exchange failed: %s",o instanceof Error?o.message:String(o)),s(e,{error:"invalid_grant",error_description:"Invalid authorization code"},400)}}catch(t){const n=t instanceof Error?t.message:String(t);return s(e,{error:"server_error",error_description:"Failed to process token request",error_details:n},500)}}}async function ee(r,e){const t=await fe(e||{},{[A.AUTHORIZATION]:r});if(t.isAuthenticated!==!0)return;const{payload:n,header:i}=C.decode(r);if(n[N.CLAIM]!==N.VALUE)return{payload:n,kid:i.kid,idpId:typeof t.idpId=="string"?t.idpId:void 0}}function H(r){return G("sha256").update(r).digest("base64url")}async function Ge(r){const e=Math.floor(Date.now()/1e3);return C.sign({type:L.MCP_CONSENT,session_hash:H(r.sessionToken),context_hash:H(r.contextToken),iat:e,exp:e+Ae},m,R.HS256)}async function Be(r,e){try{await C.verify(r,m,R.HS256)}catch{return!1}const{payload:t}=C.decode(r);return t.type===L.MCP_CONSENT&&t.session_hash===H(e.sessionToken)&&t.context_hash===H(e.contextToken)}function re(r,e){return async t=>{const n=new URL(t.req.url);let i=n.searchParams.get("context");if(!i&&n.pathname.startsWith(w(`${I.MCP_CALLBACK}/`))){const c=n.pathname.split("/");i=c[c.length-1]}if(!i)return t.text("Missing context parameter",400);let a=null;try{const c=await be(i);a=c.mcpSessionId||null;const o=c.originalRedirectUri,p=X(o);if(!c.isMcpFlow||!o||!p)throw new Error("Invalid MCP context");const d=v(t,A.AUTHORIZATION),l=d?await ee(d,r.getConfig().ssoDirect):void 0;return!d||!l?(y.sendMcpAuthorizationFailedMessage([{...M(a),error:"Authentication required",error_details:null}]),t.text("Authentication required",401)):await e(t,{contextToken:i,mcpContext:c,redirectUri:o,redirectUrl:p,sessionToken:d,session:l})}catch(c){const o=Ve(c);return y.sendMcpAuthorizationFailedMessage([{...M(a),...o}]),t.text(`Invalid MCP callback: ${o.error}`,400)}}}async function te(r,e,{mcpContext:t,redirectUri:n,redirectUrl:i,sessionToken:a,session:c}){const o=v(r,A.IDP_ACCESS_TOKEN),p=v(r,A.IDP_REFRESH_TOKEN),d=await de({idToken:a,idpId:c.idpId??Q(e.getConfig().ssoDirect),idpAccessTokenEnc:o?J(o,m):void 0,idpRefreshTokenEnc:p?V(p,m):void 0,clientId:t.mcpClientId||"",redirectUri:n,codeChallenge:t.mcpCodeChallenge||void 0,codeChallengeMethod:t.mcpCodeChallengeMethod||void 0,ttlSec:600}),l=new URL(i);return l.searchParams.set("code",d),t.mcpState&&l.searchParams.set("state",t.mcpState),y.sendMcpAuthorizationCompletedMessage([{...M(t.mcpSessionId||null),redirect_uri:n}]),r.redirect(l.toString())}function Ke(r,{mcpContext:e,redirectUrl:t}){const n=new URL(t);return n.searchParams.set("error","access_denied"),e.mcpState&&n.searchParams.set("state",e.mcpState),y.sendMcpAuthorizationFailedMessage([{...M(e.mcpSessionId||null),error:"access_denied",error_details:null}]),r.redirect(n.toString())}function Je({payload:r}){return[r.email,r.name,r.sub].find(e=>typeof e=="string"&&e.length>0)}function ne({mcpContext:r,redirectUri:e,session:t}){const{sub:n}=t.payload;if(!(!j()||typeof n!="string"||n.length===0))return{idpId:t.idpId,subject:n,clientId:r.mcpClientId||"",redirectUri:e}}function oe(r,e){const{seo:t,colorMode:n}=e.getConfig(),{logo:i}=e.getGlobalData();return{...me({seo:t,logo:i,colorMode:n}),host:new URL(D(r)).host}}function Ir(r){return re(r,async(e,t)=>{const n=ne(t);if(W(t.redirectUrl)||n&&await Oe(await r.getKv(),n))return te(e,r,t);const{mcpClientId:i,mcpClientName:a}=t.mcpContext,c=Pe({site:oe(e,r),user:Je(t.session),clientName:a||void 0,clientIdHost:z(i)?new URL(i).host:void 0,redirectUrl:t.redirectUrl,scopes:E,accessLifetimeDays:Z(!!v(e,A.IDP_REFRESH_TOKEN))/(1440*60),rememberDays:n?ve:void 0,consentToken:await Ge(t)});return new Response(c,{status:200,headers:Y})})}function wr(r){return re(r,async(e,t)=>{const n=await e.req.formData(),i=n.get("consent");if(typeof i!="string"||!await Be(i,t))return new Response(Re(oe(e,r)),{status:403,headers:Y});if(n.get("decision")==="allow"){const a=ne(t);return a&&await Ne(await r.getKv(),a),te(e,r,t)}return Ke(e,t)})}function Ve(r){return r instanceof Error&&[Te,ke,Se,Ie,we].some(e=>r instanceof e)?{error:r.name,error_details:null}:r instanceof Error?{error:r.message,error_details:r.stack??null}:{error:String(r),error_details:null}}export{F as SUPPORTED_MCP_GRANT_TYPES,He as createMcpContextToken,kr as mcpAuthorizationHandler,Ir as mcpCallbackHandler,wr as mcpConsentHandler,Tr as mcpDynamicClientRegistrationHandler,Cr as mcpOAuthAuthorizationServerHandler,gr as mcpOAuthProtectedResourceHandler,Sr as mcpTokenPortalHandler,Q as resolveMcpIdpId,be as verifyAndParseMcpContextToken,De as verifyPkce};
|