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 ![SAML Button](/img/guides/zitadel_login_saml.png) -![SAML Login](/img/guides/mocksaml_login.png) - -## 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 +![SAML Login](/img/guides/mocksaml_login.png) \ No newline at end of file