My alternative idea for this is that you can let them have their own MC account user and their own MFA but you can control the access by activating/deactivating their account user through a cloud page link.
Note: Everything below needs to be in the MC Parent context.
Step 1, you need to have/create an installed package that is enabled for the Parent BU
The installed package scope should be something like this:
PROVISIONING section
- Accounts: Read, Write
- Users: Read, Write
Get the client credentials (client id and client secret)
Step 2, create a landing page in the Parent BU
The code for the cloud page is detailed below:
2.1 Get parameters
// GET PARAMETERS
var status = Platform.Request.GetQueryStringParameter("status");
var userName = Platform.Request.GetQueryStringParameter("username");
2.2 Get v2 access token
// GET V2 ACCESS TOKEN
var debugging = false;
// Client information
var subDomain = 'YOUR SUBDOMAIN'
var client_id = 'YOUR CLIENT ID';
var client_sc = 'YOUR CLIENT SECRET';
var payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_sc
};
var url = 'https://' + subDomain + '.auth.marketingcloudapis.com/v2/token';
var req = new Script.Util.HttpRequest(url);
req.emptyContentHandling = 0;
req.retries = 2;
req.continueOnError = true;
req.contentType = "application/json";
req.method = "POST";
// auth = 'Bearer ' + accesstoken;
// req.setHeader("Authorization", auth);
req.postData = Stringify(payload);
var resp = req.send();
var resultJSON = Platform.Function.ParseJSON(String(resp.content));
if (debugging) {
Write('access token: ' + resultJSON.access_token + '<br>');
Write('expire in: ' + resultJSON.expires_in + '<br>');
};
var accesstoken = resultJSON.access_token;
2.3 get account user ID
// GET ACCOUNT USER ID
var accountUser = AccountUser.Retrieve({Property:"UserID",SimpleOperator:"equals",Value: userName});
var AccountUserID = accountUser[0].AccountUserID;
2.4 SOAP API call to update ActiveFlag attribute in AccountUser object
// CALL API TO UPDATE ActiveFlag attribute
var MID = "YOUR PARENT BU MID"; // YOUR PARENT BU MID
var URL = 'https://' + subDomain + '.soap.marketingcloudapis.com/Service.asmx';
var ContentType = 'text/xml';
var Payload = '';
Payload += '<?xml version="1.0" encoding="UTF-8"?>';
Payload += '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">';
Payload += '<s:Header>';
Payload += '<a:Action s:mustUnderstand="1">Create</a:Action>';
Payload += '<a:To s:mustUnderstand="1">https://' + subDomain + '.soap.marketingcloudapis.com/Service.asmx</a:To>';
Payload += '<fueloauth xmlns="http://exacttarget.com">' + accesstoken + '</fueloauth>';
Payload += '</s:Header>';
Payload += '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">';
Payload += '<CreateRequest xmlns="http://exacttarget.com/wsdl/partnerAPI">';
Payload += '<Options>';
Payload += '<SaveOptions>';
Payload += '<SaveOption>';
Payload += '<PropertyName>*</PropertyName>';
Payload += '<SaveAction>UpdateOnly</SaveAction>';
Payload += '</SaveOption>';
Payload += '</SaveOptions>';
Payload += '</Options> ';
Payload += '<Objects xsi:type="AccountUser">';
Payload += '<PartnerKey xsi:nil="true"></PartnerKey>';
Payload += '<ObjectID xsi:nil="true"></ObjectID>';
Payload += '<Client>';
Payload += '<ID>' + MID + '</ID>'; // Parent BU MID
Payload += '</Client>';
Payload += '<AccountUserID>' + AccountUserID + '</AccountUserID>'; // AccountUserID
Payload += '<ActiveFlag>'+ status +'</ActiveFlag>';
Payload += '</Objects>';
Payload += '</CreateRequest>';
Payload += '</s:Body>';
Payload += '</s:Envelope>';
var result = HTTP.Post(URL, ContentType, Payload);
You may wonder why we don't convert it by the WSProxy library to have a simple version then can ignore the authenticate step but trust me you cannot (I tried)
You can read about the issue in this reference and this reference also. Thank you @Eliot and @Niko!
FYI it's not a bug but SFMC did it in purpose. I was told that this is a security measure to protect the user object.
Step 3 Activate and Deactivate a user
Just need edit the username, status and go the URL on your browser.
Activate a user:
https://cloud.CloudpageURLexample.com/YOUR_SITE_KEY?username=USERNAME&status=true
Deactivate a user:
https:///cloud.CloudpageURLexample.com/YOUR_SITE_KEY?username=USERNAME&status=false
Full code
<script runat="server">
Platform.Load("core", "1.1.1");
// GET PARAMETERS
var status = Platform.Request.GetQueryStringParameter("status");
var userName = Platform.Request.GetQueryStringParameter("username");
if (status && userName){
// GET V2 ACCESS TOKEN
var debugging = false;
// Client information
var subDomain = 'YOUR SUBDOMAIN'
var client_id = 'YOUR CLIENT ID';
var client_sc = 'YOUR CLIENT SECRET';
var payload = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_sc
};
var url = 'https://' + subDomain + '.auth.marketingcloudapis.com/v2/token';
var req = new Script.Util.HttpRequest(url);
req.emptyContentHandling = 0;
req.retries = 2;
req.continueOnError = true;
req.contentType = "application/json";
req.method = "POST";
// auth = 'Bearer ' + accesstoken;
// req.setHeader("Authorization", auth);
req.postData = Stringify(payload);
var resp = req.send();
var resultJSON = Platform.Function.ParseJSON(String(resp.content));
if (debugging) {
Write('access token: ' + resultJSON.access_token + '<br>');
Write('expire in: ' + resultJSON.expires_in + '<br>');
};
var accesstoken = resultJSON.access_token;
// GET ACCOUNT USER ID
var accountUser = AccountUser.Retrieve({Property:"UserID",SimpleOperator:"equals",Value: userName});
var AccountUserID = accountUser[0].AccountUserID;
// CALL API TO UPDATE ActiveFlag attribute
var MID = "PARENT BU MID"; // YOUR PARENT BU MID
var URL = 'https://' + subDomain + '.soap.marketingcloudapis.com/Service.asmx';
var ContentType = 'text/xml';
var Payload = '';
Payload += '<?xml version="1.0" encoding="UTF-8"?>';
Payload += '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">';
Payload += '<s:Header>';
Payload += '<a:Action s:mustUnderstand="1">Create</a:Action>';
Payload += '<a:To s:mustUnderstand="1">https://' + subDomain + '.soap.marketingcloudapis.com/Service.asmx</a:To>';
Payload += '<fueloauth xmlns="http://exacttarget.com">' + accesstoken + '</fueloauth>';
Payload += '</s:Header>';
Payload += '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">';
Payload += '<CreateRequest xmlns="http://exacttarget.com/wsdl/partnerAPI">';
Payload += '<Options>';
Payload += '<SaveOptions>';
Payload += '<SaveOption>';
Payload += '<PropertyName>*</PropertyName>';
Payload += '<SaveAction>UpdateOnly</SaveAction>';
Payload += '</SaveOption>';
Payload += '</SaveOptions>';
Payload += '</Options> ';
Payload += '<Objects xsi:type="AccountUser">';
Payload += '<PartnerKey xsi:nil="true"></PartnerKey>';
Payload += '<ObjectID xsi:nil="true"></ObjectID>';
Payload += '<Client>';
Payload += '<ID>' + MID + '</ID>'; // Parent BU MID
Payload += '</Client>';
Payload += '<AccountUserID>' + AccountUserID + '</AccountUserID>'; // AccountUserID
Payload += '<ActiveFlag>'+ status +'</ActiveFlag>';
Payload += '</Objects>';
Payload += '</CreateRequest>';
Payload += '</s:Body>';
Payload += '</s:Envelope>';
var result = HTTP.Post(URL, ContentType, Payload);
Write("Status code: " + result.StatusCode + "<br>");
Write(Stringify(result.Response));
}
</script>
I hope you find it helpful!