Merge branch 'main' into pnpm-turbo-local-gen

This commit is contained in:
Max Peintner
2025-07-10 11:59:55 +02:00
13 changed files with 123 additions and 28 deletions

View File

@@ -13,7 +13,7 @@ jobs:
Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review: Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review:
- [ ] I am happy with the code - [ ] I have reviewed my changes and would approve it
- [ ] Documentations and examples are up-to-date - [ ] Documentations and examples are up-to-date
- [ ] Logical behavior changes are tested automatically - [ ] Logical behavior changes are tested automatically
- [ ] No debug or dead code - [ ] No debug or dead code
@@ -28,4 +28,4 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: content body: content
}) })

View File

@@ -9,7 +9,7 @@ plugins:
- allow_delete_body - allow_delete_body
- remove_internal_comments=true - remove_internal_comments=true
- preserve_rpc_order=true - preserve_rpc_order=true
- local: ./protoc-gen-connect-openapi - local: ./protoc-gen-connect-openapi/protoc-gen-connect-openapi
out: .artifacts/openapi3 out: .artifacts/openapi3
strategy: all strategy: all
opt: opt:

View File

@@ -218,7 +218,6 @@ module.exports = {
showLastUpdateTime: true, showLastUpdateTime: true,
editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/", editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/",
remarkPlugins: [require("mdx-mermaid")], remarkPlugins: [require("mdx-mermaid")],
docItemComponent: "@theme/ApiItem", docItemComponent: "@theme/ApiItem",
}, },
theme: { theme: {
@@ -243,6 +242,17 @@ module.exports = {
}, },
}, },
], ],
[
"@signalwire/docusaurus-plugin-llms-txt",
{
depth: 3,
logLevel: 1,
content: {
excludeRoutes: ["/search"],
enableMarkdownFiles: true,
},
},
],
[ [
"docusaurus-plugin-openapi-docs", "docusaurus-plugin-openapi-docs",
{ {

View File

@@ -30,6 +30,7 @@
"@docusaurus/theme-search-algolia": "^3.8.1", "@docusaurus/theme-search-algolia": "^3.8.1",
"@headlessui/react": "^1.7.4", "@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13", "@heroicons/react": "^2.0.13",
"@signalwire/docusaurus-plugin-llms-txt": "^1.2.0",
"@inkeep/cxkit-docusaurus": "^0.5.89", "@inkeep/cxkit-docusaurus": "^0.5.89",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"clsx": "^1.2.1", "clsx": "^1.2.1",

View File

@@ -1,5 +1,6 @@
echo $(uname -m) echo $(uname -m)
mkdir protoc-gen-connect-openapi
cd ./protoc-gen-connect-openapi/
if [ "$(uname)" = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
curl -L -o protoc-gen-connect-openapi.tar.gz https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz curl -L -o protoc-gen-connect-openapi.tar.gz https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz
else else

View File

@@ -30,6 +30,7 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst
} }
return &admin_pb.ListInstanceDomainsResponse{ return &admin_pb.ListInstanceDomainsResponse{
Result: instance_grpc.DomainsToPb(domains.Domains), Result: instance_grpc.DomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails( Details: object.ToListDetails(
domains.Count, domains.Count,
domains.Sequence, domains.Sequence,
@@ -49,6 +50,7 @@ func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.L
} }
return &admin_pb.ListInstanceTrustedDomainsResponse{ return &admin_pb.ListInstanceTrustedDomainsResponse{
Result: instance_grpc.TrustedDomainsToPb(domains.Domains), Result: instance_grpc.TrustedDomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails( Details: object.ToListDetails(
domains.Count, domains.Count,
domains.Sequence, domains.Sequence,

View File

@@ -51,8 +51,23 @@ func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedD
Offset: offset, Offset: offset,
Limit: limit, Limit: limit,
Asc: asc, Asc: asc,
SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn), SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn),
}, },
Queries: queries, Queries: queries,
}, nil }, nil
} }
func fieldNameToInstanceTrustedDomainColumn(fieldName instance.DomainFieldName) query.Column {
switch fieldName {
case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return query.InstanceTrustedDomainDomainCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return query.InstanceTrustedDomainCreationDateCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return query.InstanceTrustedDomainCreationDateCol
default:
return query.Column{}
}
}

View File

@@ -17,7 +17,11 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
} }
user, err := l.query.GetUserByLoginName(setContext(r.Context(), authReq.UserOrgID), true, authReq.LoginName) // We check if the user really exists or if it is just a placeholder or an unknown user.
// In theory, we could also check for the unknownUserID constant. However, that could disclose
// information about the existence of a user to an attacker if they check response times,
// since those requests would take shorter than the ones for real users.
user, err := l.query.GetUserByID(setContext(r.Context(), authReq.UserOrgID), true, authReq.UserID)
if err != nil { if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) { if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) {
err = nil err = nil

View File

@@ -1055,6 +1055,10 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if err != nil { if err != nil {
return nil, err return nil, err
} }
// in case the user was set automatically, we might not have the org set
if request.UserOrgID == "" {
request.UserOrgID = user.ResourceOwner
}
userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user) userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,22 +1,41 @@
import { getSAMLFormCookie } from "@/lib/saml";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const url = searchParams.get("url"); const url = searchParams.get("url");
const relayState = searchParams.get("RelayState"); const id = searchParams.get("id");
const samlResponse = searchParams.get("SAMLResponse");
if (!url || !relayState || !samlResponse) { if (!url) {
return new NextResponse("Missing required parameters", { status: 400 }); return new NextResponse("Missing url parameter", { status: 400 });
} }
if (!id) {
return new NextResponse("Missing id parameter", { status: 400 });
}
const formData = await getSAMLFormCookie(id);
const formDataParsed = formData ? JSON.parse(formData) : null;
if (!formDataParsed) {
return new NextResponse("SAML form data not found", { status: 404 });
}
// Generate hidden input fields for all key-value pairs in formDataParsed
const hiddenInputs = Object.entries(formDataParsed)
.map(
([key, value]) =>
`<input type="hidden" name="${key}" value="${value}" />`,
)
.join("\n ");
// Respond with an HTML form that auto-submits via POST // Respond with an HTML form that auto-submits via POST
const html = ` const html = `
<html> <html>
<body onload="document.forms[0].submit()"> <body onload="document.forms[0].submit()">
<form action="${url}" method="post"> <form action="${url}" method="post">
<input type="hidden" name="RelayState" value="${relayState}" /> ${hiddenInputs}
<input type="hidden" name="SAMLResponse" value="${samlResponse}" />
<noscript> <noscript>
<button type="submit">Continue</button> <button type="submit">Continue</button>
</noscript> </noscript>

View File

@@ -520,16 +520,24 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") { if (url && binding.case === "redirect") {
return NextResponse.redirect(url); return NextResponse.redirect(url);
} else if (url && binding.case === "post") { } else if (url && binding.case === "post") {
const redirectUrl = constructUrl(request, "/saml-post"); // Create HTML form that auto-submits via POST and escape the SAML cookie
const html = `
<html>
<body onload="document.forms[0].submit()">
<form action="${url}" method="post">
<input type="hidden" name="RelayState" value="${binding.value.relayState}" />
<input type="hidden" name="SAMLResponse" value="${binding.value.samlResponse}" />
<noscript>
<button type="submit">Continue</button>
</noscript>
</form>
</body>
</html>
`;
redirectUrl.searchParams.set("url", url); return new NextResponse(html, {
redirectUrl.searchParams.set("RelayState", binding.value.relayState); headers: { "Content-Type": "text/html" },
redirectUrl.searchParams.set( });
"SAMLResponse",
binding.value.samlResponse,
);
return NextResponse.redirect(redirectUrl.toString());
} else { } else {
console.log( console.log(
"could not create response, redirect user to choose other account", "could not create response, redirect user to choose other account",

View File

@@ -4,7 +4,9 @@ import { createResponse, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { constructUrl } from "./service-url"; import { constructUrl } from "./service-url";
import { isSessionValid } from "./session"; import { isSessionValid } from "./session";
@@ -17,6 +19,37 @@ type LoginWithSAMLAndSession = {
request: NextRequest; request: NextRequest;
}; };
export async function getSAMLFormUID() {
return uuidv4();
}
export async function setSAMLFormCookie(value: string): Promise<string> {
const cookiesList = await cookies();
const uid = await getSAMLFormUID();
await cookiesList.set({
name: uid,
value: value,
httpOnly: true,
path: "/",
maxAge: 5 * 60, // 5 minutes
});
return uid;
}
export async function getSAMLFormCookie(uid: string): Promise<string | null> {
const cookiesList = await cookies();
const cookie = cookiesList.get(uid);
if (!cookie || !cookie.value) {
return null;
}
return cookie.value;
}
export async function loginWithSAMLAndSession({ export async function loginWithSAMLAndSession({
serviceUrl, serviceUrl,
samlRequest, samlRequest,

View File

@@ -52,6 +52,7 @@ import {
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { unstable_cacheLife as cacheLife } from "next/cache"; import { unstable_cacheLife as cacheLife } from "next/cache";
import { getUserAgent } from "./fingerprint"; import { getUserAgent } from "./fingerprint";
import { setSAMLFormCookie } from "./saml";
import { createServiceForHost } from "./service"; import { createServiceForHost } from "./service";
const useCache = process.env.DEBUG !== "true"; const useCache = process.env.DEBUG !== "true";
@@ -981,18 +982,15 @@ export async function startIdentityProviderFlow({
value: urls, value: urls,
}, },
}) })
.then((resp) => { .then(async (resp) => {
if (resp.nextStep.case === "authUrl" && resp.nextStep.value) { if (resp.nextStep.case === "authUrl" && resp.nextStep.value) {
return resp.nextStep.value; return resp.nextStep.value;
} else if (resp.nextStep.case === "formData" && resp.nextStep.value) { } else if (resp.nextStep.case === "formData" && resp.nextStep.value) {
const formData: FormData = resp.nextStep.value; const formData: FormData = resp.nextStep.value;
const redirectUrl = "/saml-post"; const redirectUrl = "/saml-post";
const params = new URLSearchParams({ url: formData.url }); const dataId = await setSAMLFormCookie(JSON.stringify(formData.fields));
const params = new URLSearchParams({ url: formData.url, id: dataId });
Object.entries(formData.fields).forEach(([k, v]) => {
params.append(k, v);
});
return `${redirectUrl}?${params.toString()}`; return `${redirectUrl}?${params.toString()}`;
} else { } else {