Compare commits

...
Author SHA1 Message Date
shadowdaoandClaude Opus 5 ba9d8fbe60 fix: accept the issuer with or without a trailing slash
Regression introduced with OIDC_ISSUER_MCP. mcpIssuer() stripped the trailing
slash — right for building the JWKS URL, wrong for the `iss` claim check,
which jose compares by exact string. Authentik emits
`.../application/o/shared-memory-mcp/` with the slash, so verification failed
with "claim invalid: iss" even though issuer and audience were both correct.

Before OIDC_ISSUER_MCP the issuer was passed to jwtVerify unstripped and only
stripped when constructing the URL; collapsing both onto the stripped form is
what broke it.

mcpIssuer() now returns the value as configured, the JWKS URL strips locally,
and the claim check accepts both spellings so correctness doesn't hinge on
whether someone typed a trailing slash into an env var.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:42:30 -07:00
jknapp c12250fd11 Merge pull request 'docs: describe project sharing as shipped on the settings page' (#16) from docs/settings-groups-copy into main 2026-07-27 13:35:43 +00:00
shadowdaoandClaude Opus 5 8361a5b8e2 docs: describe project sharing as shipped on the settings page
The Groups card still called sharing "the upcoming sharing feature". It has
shipped — memory_visibility, groups, user_groups and project_shares are all
live — so the card now describes what group membership actually does:
read access for member groups, write access for read-write groups.

Authored on the fix/memory-list-project-filter branch on 2026-07-05 and never
committed; that branch is otherwise fully merged via PR #7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:35:42 -07:00
jknapp 04b964a866 Merge pull request 'feat: add the logo, and point people at the plugin before they mint a token' (#15) from feat/logo-and-tokens-copy into main 2026-07-27 13:22:52 +00:00
3 changed files with 25 additions and 5 deletions
+3 -1
View File
@@ -63,7 +63,9 @@ export default async function SettingsPage() {
</CardHeader> </CardHeader>
<CardBody className="text-sm text-fg-muted"> <CardBody className="text-sm text-fg-muted">
OIDC group memberships from your IdP, refreshed at sign-in. Used OIDC group memberships from your IdP, refreshed at sign-in. Used
by the upcoming sharing feature to scope project visibility. to scope project sharing &mdash; memories and snippets in a shared
project are readable by member groups and editable by read-write
groups.
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
@@ -26,7 +26,7 @@ export function GET() {
// The MCP application's issuer, which is not necessarily the Web UI's — // The MCP application's issuer, which is not necessarily the Web UI's —
// see mcpIssuer(). Advertising the wrong one sends clients to a discovery // see mcpIssuer(). Advertising the wrong one sends clients to a discovery
// document whose tokens this endpoint will then reject on `iss`. // document whose tokens this endpoint will then reject on `iss`.
authorization_servers: [mcpIssuer()], authorization_servers: [mcpIssuer()], // as configured, slash and all
scopes_supported: ["openid", "profile", "email", audienceScope], scopes_supported: ["openid", "profile", "email", audienceScope],
bearer_methods_supported: ["header"], bearer_methods_supported: ["header"],
resource_documentation: `${resource}/`, resource_documentation: `${resource}/`,
+21 -3
View File
@@ -29,7 +29,25 @@ const g = globalThis as GlobalWithJwks;
* application slug, so this is NOT interchangeable with OIDC_ISSUER. * application slug, so this is NOT interchangeable with OIDC_ISSUER.
*/ */
export function mcpIssuer(): string { export function mcpIssuer(): string {
return (env().OIDC_ISSUER_MCP ?? env().OIDC_ISSUER).replace(/\/$/, ""); return env().OIDC_ISSUER_MCP ?? env().OIDC_ISSUER;
}
/**
* Issuer values accepted for the `iss` claim.
*
* jose compares `iss` by exact string, and IdPs are inconsistent about the
* trailing slash: Authentik emits `.../application/o/<slug>/` while the same
* value is routinely configured without it. Normalizing to one form and
* comparing against that fails whenever the two disagree — which is exactly
* how this broke: the URL-safe (stripped) form was reused for the claim check
* against a token whose `iss` ended in a slash.
*
* Accept both spellings rather than making correctness depend on how someone
* typed an env var.
*/
function acceptedIssuers(): [string, string] {
const bare = mcpIssuer().replace(/\/$/, "");
return [bare, `${bare}/`];
} }
function jwks() { function jwks() {
@@ -37,7 +55,7 @@ function jwks() {
// Authentik discovery is at `${issuer}/.well-known/openid-configuration`; // Authentik discovery is at `${issuer}/.well-known/openid-configuration`;
// the JWKS URI is normally `${issuer}/jwks/` or `${issuer}/.well-known/jwks.json`. // the JWKS URI is normally `${issuer}/jwks/` or `${issuer}/.well-known/jwks.json`.
// Authentik canonically serves `${issuer}/jwks/`. // Authentik canonically serves `${issuer}/jwks/`.
const url = new URL(`${mcpIssuer()}/jwks/`); const url = new URL(`${mcpIssuer().replace(/\/$/, "")}/jwks/`);
g.__sharedMemoryJwks = createRemoteJWKSet(url, { g.__sharedMemoryJwks = createRemoteJWKSet(url, {
cacheMaxAge: 10 * 60 * 1000, // 10 min cacheMaxAge: 10 * 60 * 1000, // 10 min
cooldownDuration: 30 * 1000, cooldownDuration: 30 * 1000,
@@ -126,7 +144,7 @@ export async function authenticateBearer(authHeader: string | null): Promise<Aut
} }
const { payload } = await jwtVerify(token, jwks(), { const { payload } = await jwtVerify(token, jwks(), {
issuer: mcpIssuer(), issuer: acceptedIssuers(),
audience: env().OIDC_AUDIENCE, audience: env().OIDC_AUDIENCE,
}); });
if (!payload.sub) { if (!payload.sub) {