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
This commit is contained in:
Gayathri Vijayan
2025-12-24 09:38:17 +01:00
committed by GitHub
parent 3a2eb96e82
commit d14d1df529
@@ -56,18 +56,219 @@ They are available under `https://${CUSTOMDOMAIN}/idps/\{ID of the provider in Z
<CustomLoginPolicy/>
## 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)
<PrefillAction fields="firstname, lastname, email, display name and username" provider="mockSaml"/>
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
<TestSetup loginscreen="your SAML login"/>
![SAML Button](/img/guides/zitadel_login_saml.png)
![SAML Login](/img/guides/mocksaml_login.png)
## Optional: Add ZITADEL action to autofill userdata
<PrefillAction fields="firstname, lastname, email, display name and username" provider="mockSaml"/>
```js reference
https://github.com/zitadel/actions/blob/main/examples/post_auth_saml.js
```
![SAML Login](/img/guides/mocksaml_login.png)