mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -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"/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||

|
||||
Reference in New Issue
Block a user