mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 17:57:33 +00:00
Merge branch 'main' into pnpm-turbo-local-gen
This commit is contained in:
2
.github/workflows/ready_for_review.yml
vendored
2
.github/workflows/ready_for_review.yml
vendored
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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",
|
||||||
{
|
{
|
||||||
|
@@ -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",
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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",
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user