From d14d1df5290c1d76e4538e409ee2acc78d03a5ee Mon Sep 17 00:00:00 2001
From: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com>
Date: Wed, 24 Dec 2025 09:38:17 +0100
Subject: [PATCH] docs: update MockSAML IDP documentation (#11226)
# Which Problems Are Solved
The current documentation on setting up a MockSAML IDP is
unclear/incomplete, and doesn't work with Login V2.
# How the Problems Are Solved
- Add a new section that explains mapping of MockSAML attributes to
Zitadel user fields (using Actions V2) in case of Login V2
- Update the Actions V1 script used to prefill user data in case of
Login V1
# Additional Changes
N/A
# Additional Context
- Related to https://github.com/zitadel/zitadel/issues/11078
---
.../integrate/identity-providers/mocksaml.mdx | 219 +++++++++++++++++-
1 file changed, 210 insertions(+), 9 deletions(-)
diff --git a/docs/docs/guides/integrate/identity-providers/mocksaml.mdx b/docs/docs/guides/integrate/identity-providers/mocksaml.mdx
index 4cdfa90ad..52f1f410f 100644
--- a/docs/docs/guides/integrate/identity-providers/mocksaml.mdx
+++ b/docs/docs/guides/integrate/identity-providers/mocksaml.mdx
@@ -56,18 +56,219 @@ They are available under `https://${CUSTOMDOMAIN}/idps/\{ID of the provider in Z
+## Configure an action to autofill user data
+
+### Required for Login V2
+The creation of users in ZITADEL will fail if the required fields to create a user are not set.
+
+**Actions V2** can be used to map the SAML attributes returned by the IDP to the required fields in ZITADEL.
+See the [Actions V2 Response Manipulation](/guides/integrate/actions/testing-response-manipulation.md)
+guide for more information on setting up a Target and an Execution. In short,
+* Create an Actions V2 Target of type `REST Call`
+* Create an Execution of type `Response` on the method `/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent`
+
+The following minimal example modifies the response of `/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent` to set the required fields
+for user creation. This example is specific to MockSAML, please adjust the attributes according to your IDP.
+
+``` go
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/muhlemmer/gu"
+ "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2"
+ "google.golang.org/protobuf/encoding/protojson"
+)
+
+type contextResponse struct {
+ Request *retrieveIdentityProviderIntentRequestWrapper `json:"request"`
+ Response *retrieveIdentityProviderIntentResponseWrapper `json:"response"`
+}
+
+// RetrieveIdentityProviderIntentRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly
+type retrieveIdentityProviderIntentRequestWrapper struct {
+ user.RetrieveIdentityProviderIntentRequest
+}
+
+func (r *retrieveIdentityProviderIntentRequestWrapper) MarshalJSON() ([]byte, error) {
+ data, err := protojson.Marshal(r)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+func (r *retrieveIdentityProviderIntentRequestWrapper) UnmarshalJSON(data []byte) error {
+ return protojson.Unmarshal(data, r)
+}
+
+// RetrieveIdentityProviderIntentResponseWrapper necessary to marshal and unmarshal the JSON into the proto message correctly
+type retrieveIdentityProviderIntentResponseWrapper struct {
+ user.RetrieveIdentityProviderIntentResponse
+}
+
+func (r *retrieveIdentityProviderIntentResponseWrapper) MarshalJSON() ([]byte, error) {
+ data, err := protojson.Marshal(r)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+func (r *retrieveIdentityProviderIntentResponseWrapper) UnmarshalJSON(data []byte) error {
+ return protojson.Unmarshal(data, r)
+}
+
+// call HandleFunc to read the response body, manipulate the content and return the response
+func call(w http.ResponseWriter, req *http.Request) {
+ // read the body content
+ sentBody, err := io.ReadAll(req.Body)
+ if err != nil {
+ // if there was an error while reading the body return an error
+ http.Error(w, "error", http.StatusInternalServerError)
+ return
+ }
+ defer req.Body.Close()
+
+ // read the response into the expected structure
+ request := new(contextResponse)
+ if err := json.Unmarshal(sentBody, request); err != nil {
+ http.Error(w, "error", http.StatusInternalServerError)
+ return
+ }
+
+ // build the response from the received response
+ resp := request.Response
+ // manipulate the received response to send back as response
+ if err = manipulateResponse(resp); err != nil {
+ http.Error(w, "error modifying response", http.StatusInternalServerError)
+ return
+ }
+
+ // marshal the response into json
+ data, err := json.Marshal(resp)
+ if err != nil {
+ // if there was an error while marshalling the json
+ http.Error(w, "error marshaling response", http.StatusInternalServerError)
+ return
+ }
+
+ // return the manipulated response
+ w.Write(data)
+}
+
+type rawInformation struct {
+ Attributes struct {
+ Email []string `json:"email"`
+ FirstName []string `json:"firstName"`
+ Id []string `json:"id"`
+ LastName []string `json:"lastName"`
+ } `json:"attributes"`
+ Id string `json:"id"`
+}
+
+func manipulateResponse(resp *retrieveIdentityProviderIntentResponseWrapper) error {
+ if resp == nil || resp.IdpInformation == nil || resp.IdpInformation.RawInformation == nil {
+ log.Println("missing IDP/User information in the IDP response")
+ return nil
+ }
+ // retrieve raw information from the IDP
+ var rawInfoBytes []byte
+ var err error
+ if rawInfoBytes, err = resp.IdpInformation.RawInformation.MarshalJSON(); err != nil {
+ return err
+ }
+
+ // NOTE: the raw information struct used here is specific to MockSAML.
+ // Please adapt this to your IDP.
+ var rawInfo rawInformation
+ if err = json.Unmarshal(rawInfoBytes, &rawInfo); err != nil {
+ return err
+ }
+
+ if len(rawInfo.Attributes.Email) == 0 ||
+ len(rawInfo.Attributes.FirstName) == 0 ||
+ len(rawInfo.Attributes.Id) == 0 ||
+ len(rawInfo.Attributes.LastName) == 0 {
+ log.Println("missing required attributes in the IDP response")
+ return nil
+ }
+
+ // to create a new user.
+ if resp.AddHumanUser != nil {
+ // set the required fields to create a new user in Zitadel based on the information returned by the IDP
+ resp.IdpInformation.UserName = rawInfo.Attributes.Email[0]
+
+ resp.AddHumanUser.Profile = &user.SetHumanProfile{
+ GivenName: rawInfo.Attributes.FirstName[0],
+ FamilyName: rawInfo.Attributes.LastName[0],
+ }
+ resp.AddHumanUser.Email = &user.SetHumanEmail{
+ Email: rawInfo.Attributes.Email[0],
+ Verification: &user.SetHumanEmail_IsVerified{IsVerified: true},
+ }
+ resp.AddHumanUser.Username = gu.Ptr(rawInfo.Attributes.Email[0])
+ resp.AddHumanUser.IdpLinks = []*user.IDPLink{
+ {
+ UserName: rawInfo.Attributes.Email[0],
+ },
+ }
+ }
+ return nil
+}
+
+func main() {
+ // handle the HTTP call under "/call"
+ http.HandleFunc("/call", call)
+
+ // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
+ http.ListenAndServe(":8090", nil)
+}
+
+```
+
+
+
+### Optional for Login V1 (using Actions V1)
+
+
+
+NOTE: The name of the action should be the same as the function name. In this example, the action should be named `mapSAMLIDPAttrs`
+
+```js
+function mapSAMLIDPAttrs(ctx, api) {
+ if (ctx.v1.externalUser.externalIdpId != "external-idp-id") {
+ return
+ }
+ // the attribute names below represent the ones used by MockSAML, please adjust accordingly for your IDP.
+ let firstname = ctx.v1.providerInfo.attributes["firstName"];
+ let lastname = ctx.v1.providerInfo.attributes["lastName"];
+ let email = ctx.v1.providerInfo.attributes["email"];
+ let username = ctx.v1.providerInfo.attributes["email"];
+ if (firstname != undefined) {
+ api.setFirstName(firstname[0]);
+ }
+ if (lastname != undefined) {
+ api.setLastName(lastname[0]);
+ }
+ if (email != undefined) {
+ api.setEmail(email[0]);
+ api.setEmailVerified(true);
+ }
+ if (username != undefined) {
+ api.setPreferredUsername(username[0]);
+ }
+}
+```
+
## Test the setup

-
-
-## Optional: Add ZITADEL action to autofill userdata
-
-
-
-```js reference
-https://github.com/zitadel/actions/blob/main/examples/post_auth_saml.js
-```
\ No newline at end of file
+
\ No newline at end of file