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:
- [ ] I am happy with the code
- [ ] I have reviewed my changes and would approve it
- [ ] Documentations and examples are up-to-date
- [ ] Logical behavior changes are tested automatically
- [ ] No debug or dead code
@@ -28,4 +28,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: content
})
})

View File

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

View File

@@ -218,7 +218,6 @@ module.exports = {
showLastUpdateTime: true,
editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/",
remarkPlugins: [require("mdx-mermaid")],
docItemComponent: "@theme/ApiItem",
},
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",
{

View File

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

View File

@@ -1,5 +1,6 @@
echo $(uname -m)
mkdir protoc-gen-connect-openapi
cd ./protoc-gen-connect-openapi/
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
else

View File

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

View File

@@ -51,8 +51,23 @@ func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedD
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn),
SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn),
},
Queries: queries,
}, 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)
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 authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) {
err = nil

View File

@@ -1055,6 +1055,10 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if err != nil {
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)
if err != nil {
return nil, err

View File

@@ -1,22 +1,41 @@
import { getSAMLFormCookie } from "@/lib/saml";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const url = searchParams.get("url");
const relayState = searchParams.get("RelayState");
const samlResponse = searchParams.get("SAMLResponse");
const id = searchParams.get("id");
if (!url || !relayState || !samlResponse) {
return new NextResponse("Missing required parameters", { status: 400 });
if (!url) {
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
const html = `
<html>
<body onload="document.forms[0].submit()">
<form action="${url}" method="post">
<input type="hidden" name="RelayState" value="${relayState}" />
<input type="hidden" name="SAMLResponse" value="${samlResponse}" />
${hiddenInputs}
<noscript>
<button type="submit">Continue</button>
</noscript>

View File

@@ -520,16 +520,24 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} 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);
redirectUrl.searchParams.set("RelayState", binding.value.relayState);
redirectUrl.searchParams.set(
"SAMLResponse",
binding.value.samlResponse,
);
return NextResponse.redirect(redirectUrl.toString());
return new NextResponse(html, {
headers: { "Content-Type": "text/html" },
});
} else {
console.log(
"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 { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { constructUrl } from "./service-url";
import { isSessionValid } from "./session";
@@ -17,6 +19,37 @@ type LoginWithSAMLAndSession = {
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({
serviceUrl,
samlRequest,

View File

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