Anytime you need to bring a batch of new users into a tenant at once a large new-hire cohort, a bulk migration, a group of external users who need day-one access you run into the same identity problem: how do you get everyone a usable credential securely, without a static password sitting in someone's inbox?
This post explains how I automated the process with Microsoft Entra ID Temporary Access Pass (TAP), Microsoft Graph, and Power Automate. My requirement is bulk onboarding, but it can also serve as the joiner trigger in the IGA process for general onboarding.
The problem
When a batch of new accounts lands in your Entra ID tenant, those people have zero trust relationship with your organization yet. You don't have their passwords, you can't assume their personal email is secure, and you generally don't know if their mobile number has been verified by anyone. So how do you get them a usable credential?
The answer most identity teams land on is a Temporary Access Pass (TAP) a time-boxed, single-use code that lets someone sign in and register their real credentials (Passkey, Authenticator, Windows Hello for Business) without ever touching a static password.
Additionally, if your organization has started rolling out phishing-resistant MFA, this is the best way to onboard new joiners and enforce conditional access so MFA is used only for phishing-resistant MFA. For the conditional access part, I’ll add a link at the bottom there.
The remaining problem is delivery. If you email someone their TAP and their sign-in instructions in the same message, you've just made one compromised mailbox equivalent to a compromised account. So the design principle I built around was simple:
Never put the credential and the instructions in the same channel.
The architecture
The pipeline has four moving parts:
- A Microsoft Entra ID app registration — a scoped service identity that calls Microsoft Graph on the flow's behalf
- Azure Key Vault — holds the app's client secret so it never sits in plaintext inside a flow definition
- A SharePoint list — the new-user roster, which also doubles as an audit log (I’m using this because of limitations in my test environment, where Workday or the IGA tool isn’t provisioned.)
- A Power Automate flow — the orchestrator
The sequence, once a new user row lands in SharePoint: For the test environment, SharePoint is currently the trigger point, but based on the requirements, we can move it to either Workday or an API connection.
- Get an OAuth token for Microsoft Graph (client credentials flow, secret pulled live from Key Vault)
- Resolve the user's Entra ID object ID from their UPN
- Generate a TAP for that user — single-use, capped lifetime, and optionally a delayed start time matched to their actual start date
- Send an email immediately with sign-in instructions. Additionally, we can arrange for an SMS connector and TAP to be delivered.
- Wait a few minutes
- Log the outcome back to the SharePoint row
Prerequisites:
- Azure Subscription
- Power Automate Licensing for premium connectors
- [Optional] SMS Gateway Subscription
Step by Step Configuration
Step 1: App registration for Graph API access
- Go to Entra ID > App registrations > New registration
- Name it something like TAP-Automation-Onboarding
- API permissions needed: UserAuthenticationMethod.ReadWrite.All, User.Read.All (application, not delegated, since this runs unattended)
- Get admin consent
- Go to Certificates & secrets and generate the New Client Secret
- Note down the Value
- go to Overview and note down the Client ID and Tenant ID as well
Step 2: Trigger
- Create a SharePoint Site and SharePoint List. List should content blow columns
- ID Rename with UPN
- Name - Single line of text
- PersonalEmail - Single line of text
- JoinDate - Date and Time
- MobileNo - Single line of text
- Status - Single line of text
- TAP Generated - Date and Time
- Email Sent - Yes/No
- SMS Sent - Yes/No
Step 3: Create the flow
Acquisition-TAP-OnboardingStep 4: Initialize variables
| Name | Type | Value (set now or later from trigger) |
|---|---|---|
varUPN | String | dynamic content: trigger's email/UPN field |
varMobile | String | dynamic content: trigger's mobile field |
varAccessToken | String | leave blank |
varTAP | String | leave blank |
varUserId | String | leave blank |
varRecipientEmail | String | dynamic content: trigger's email field |
varJoinDate | String | leave blank |
Set varUPN, so the canvas is readable.Step 5: Create an Azure Vault
- Go to portal.azure.com
- Subscriptions > open your subscription > Access control (IAM) > Add > Add role assignment
- Search for Key Vault Secrets Officer, assign it to your own account (lets you manage secrets in any vault under this subscription)
- Search for Key Vault in the top search bar > Create
- Name:
kv-tap-onboarding-wiley - Choose the correct subscription, resource group, and region > Review + create
- Once deployed, open
kv-tap-onboarding-wiley> Access control (IAM) > Add > Add role assignment - Search for Key Vault Secrets User, assign it to your automation app's service principal (search for
TAP-Automation-Onboarding)
Part 6: Get the client secret from Key Vault
- Go back to
- + > Add an action > search
Azure Key Vault - Select Get secret
- If prompted to create a connection:
- Connection name:
Azure Key Vault - Get secret - Authentication type: Service Principal (Client Secret) — not Client Certificate Auth
- Vault name: your vault name
- Client ID: your app registration's Application (client) ID
- Tenant: your tenant ID
- Client Secret: paste the client secret value directly here (this one exception is fine, as discussed)
- Click Create
- Connection name:
- In the action body, Name of the secret: type
TAP-Automation-ClientSecretexactly as named in the vault. (This is what we have created on step 5) - Rename action:
Get Client Secret
Save.
Part 7: Get an OAuth token from Microsoft Entra ID
- + > Add an action > search
HTTP, filter to Built-in if both connector and built-in versions show up, select the plain HTTP action (icon is a simple shape, no service logo) - Method:
POST - URI:
https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/token(Replace with Tenant ID)
4. Headers: Key Content-Type, Value application/x-www-form-urlencoded
5. Body: type this, then insert the dynamic token where indicated:
grant_type=client_credentials&client_id=YOUR-APP-CLIENT-ID&client_secret=Click right after client_secret=, press /, select Get Client Secret > value. Then continue typing after the inserted token:
&scope=https://graph.microsoft.com/.default- Rename action:
Get OAuth Token
Save.
Part 8: Parse the token response
- + > Add an action > search
Parse JSON(Data Operations, built-in) - Content: click in, press
/, select Get OAuth Token > Body - Schema: click "Use sample payload to generate schema", paste:
{"type": "object","properties": {"token_type": {"type": "string"},"expires_in": {"type": "integer"},"ext_expires_in": {"type": "integer"},"access_token": {"type": "string"}}}
- Click Done
- Rename action:
Parse Token Response
Save.
Part 9: Store the access token
- + > Add an action > search
Set variable - Name: select
varAccessToken - Value: click in, press
/, select access_token under Parse Token Response's outputs - Rename action:
Set varAccessToken
Save.
Part 10: Look up the acquired user's object ID in Entra ID
- + > Add an action > search
HTTP(built-in again) - Method:
GET - URI:
https://graph.microsoft.com/v1.0/users/@{variables('varUPN')}(insert varUPN via dynamic content/expression inside the URI field)
4. Headers: Key Authorization, Value: type Bearer then insert varAccessToken dynamic content right after the space
5. Rename action: Get User Object ID
Save.
Part 11: Parse the user lookup response
- + > Add action >
Parse JSON - Content:
Get User Object ID > Body - Schema: sample:
{"type": "object","properties": {"id": {"type": "string"},"userPrincipalName": {"type": "string"}}}
- Rename:
Parse User Lookup
Save.
Part 12: Store the user's object ID
- + > Add action >
Set variable - Name:
varUserId - Value:
idfrom Parse User Lookup's outputs - Rename:
Set varUserId
Save.
Part 13: Generate the TAP
- + > Add action >
HTTP(built-in) - Method:
POST - URI:
https://graph.microsoft.com/v1.0/users/@{variables('varUserId')}/authentication/temporaryAccessPassMethods- Headers:
Authorization:Bearer+ varAccessToken dynamic contentContent-Type:application/json
- Body:
{
"lifetimeInMinutes": 480,
"isUsableOnce": true,
"startDateTime": "@{variables('varJoinDate')}"
}- Rename:
Generate TAP
Save.
Note: Lifetimeinminutes follows the default TAP life time. In my environment, it is 8hr.
Part 14: Parse the user lookup response
- + > Add action >
Parse JSON - Content:
Generate TAP > Body > Body - Schema: sample:
{"type": "object","properties": {"id": {"type": "string"},"isUsable": {"type": "boolean"},"methodUsabilityReason": {"type": "string"},"temporaryAccessPass": {"type": "string"},"createdDateTime": {"type": "string"},"startDateTime": {"type": "string"},"lifetimeInMinutes": {"type": "integer"},"isUsableOnce": {"type": "boolean"}}}
- Rename:
Parse User Lookup
Save.
Part 15: Store the TAP Value
- + > Add action >
Set variable - Name:
varTAP - Value:
idfrom Parse Tap Response outputs - Rename:
Set varTAP
Part 16: Send the email leg
- + > Add action > search
Send an email (V2)(Outlook connector) - To:
varRecipientEmailor trigger's email field - Subject:
Your XXX account is ready - Body:
Hello,
Your account has been created: [insert varUPN dynamic content]
Sign in at: https://myaccount.microsoft.com
You will receive a one-time access code via SMS shortly. Do not share this code with anyone, including IT staff.
Regards,
IT Security- Rename:
Send Email Instructions
Save.
Part 17: Delay
- + > Add action >
Delay - Count:
5, Unit:Minute
Save.
Part 18: Send the SMS leg (TAP only)
Requires an Azure Communication Services connection with a provisioned phone number, we haven't built this yet. When ready:
- + > Add action > search
Azure Communication ServicesorSend SMS - Set up connection with your ACS connection string
- From: your ACS number
- To:
varMobiledynamic content - Message:
Your Wiley access code:+ varTAP dynamic content +. Valid 24 hours, single use only. - Rename:
Send SMS TAP
Save.
Part 19: Log the outcome
- + > Add action >
Update item(SharePoint) orUpdate a row(Dataverse), targeting the same row - Set fields for status/timestamp
Save.
In the SharePoint columns we created for status, the SMS (Yes/No) and Email (Yes/No) fields will be filled in by this flow.
Click the Test button on the top and do a manual test. once you update the sharepoint list this should send a email notification. for the email please check the Junk as if you are sending to your personal email ID. these can be flag as spam.
NOTE:
In my scenario, I have built up to part 16 only. As per our requirement, we will deliver this to the hiring manager, who will verify it and share the information with the candidate. Additionally, for bulk onboarding, we only use the personal email ID as the acquisition email ID, so both the TAP and the email ID are delivered through email.
Additionally, for SMS, you need to provision Azure Communication Services first, or use another connector that supports SMS. Both options require a paid subscription to send SMS, which I have not covered here since my organization's requirement is only to send email at this moment. However, I believe SMS is the better way to share the TAP.
Endless possibilities
- Rather than triggering from SharePoint, there are connectors available for Workday or HTTP APIs to trigger from your organization's source of truth or IGA tool. You only need to change the trigger, and the rest of the workflow can remain the same.
- For delivering to hiring managers, you can get a variable to set the end user's manager and share this with the manager as well.
- SMS or secure link delivery methods can be integrated based on your requirement.


0 comments:
Post a Comment