Introduction In Governance, Risk, and Compliance (GRC), having a detailed overview of user accounts in Azure Entra ID is crucial. This blog post will guide you through generating a user report using Microsoft Graph API in Go. The report includes essential details such as user authentication methods, role assignments, and password policies, making it useful for security assessments and compliance audits.
Prerequisites
Before we begin, ensure you have:
- An Azure account with Entra ID access
- A registered application with the necessary Microsoft Graph API permissions
- Go installed with the Microsoft Graph SDK
Step 1: Creating Graph Client
The first step is creating Graph API client:
creds, err := azidentity.NewClientSecretCredential(
tenant.TenantId,
tenant.ClientId,
tenant.Secret,
nil,
)
if err != nil {
log.Fatalf("Error creating credentials for tenant %s: %v", tenant.TenantId, err)
}
client, err := msgraphsdk.NewGraphServiceClientWithCredentials(creds, []string{"https://graph.microsoft.com/.default"})
if err != nil {
log.Fatalf("Error creating client for tenant %s: %v", tenant.TenantId, err)
}
Step 2: Fetch All Users
After that we need to retrieve user details from Azure Entra ID using the Graph API. We will request specific fields and expand the MemberOf property to identify group memberships. In order to get all the users we need to ask additional pages from API using GetOdataNextLink:
Note: The MemberOf property in Microsoft Graph API only provides direct group memberships for a user. If you need to retrieve nested group memberships, you will have to perform additional queries to resolve group >hierarchies.
userProps := []string{
"Id",
"Mail",
"DisplayName",
"UserType",
"AccountEnabled",
"SignInActivity",
"PasswordPolicies",
"LastPasswordChangeDateTime",
"MemberOf",
"AssignedLicenses",
"JobTitle",
"Department",
"CreatedDateTime",
"UserPrincipalName",
"OnPremisesSyncEnabled",
}
filter := &users.UsersRequestBuilderGetRequestConfiguration{
QueryParameters: &users.UsersRequestBuilderGetQueryParameters{
Select: userProps,
Expand: []string{"MemberOf"},
},
}
usersResp, err := client.Users().Get(ctx, filter)
if err != nil {
log.Fatalf("error getting users: %v", err)
}
users := usersResp.GetValue()
for {
nextPageUrl := usersResp.GetOdataNextLink()
if nextPageUrl != nil {
usersResp, err = client.Users().WithUrl(*nextPageUrl).Get(ctx, filter)
if err != nil {
log.Printf("error getting users: %v", err)
continue
}
users = append(users, usersResp.GetValue()...)
} else {
break
}
}
return users
Step 3: Retrieve User Roles
Next, we need to get role assignments for each user.
roleReps, err := client.DirectoryRoles().Get(ctx, nil)
if err != nil {
log.Fatalf("error getting roles: %v", err)
}
roles := roleReps.GetValue()
for {
nextPageUrl := roleReps.GetOdataNextLink()
if nextPageUrl != nil {
rolesReps, err = client.DirectoryRoles().WithUrl(*nextPageUrl).Get(ctx, nil)
if err != nil {
log.Printf("error getting roles: %v", err)
continue
}
roles = append(roles, roleReps.GetValue()...)
} else {
break
}
}
return roles
Step 4: Fetch Authentication Reports
To enhance the report, we retrieve authentication methods and MFA details for all users.
authResp, err := client.Reports().AuthenticationMethods().UserRegistrationDetails().Get(ctx, nil)
if err != nil {
log.Fatalf("error getting auth reports: %v", err)
}
authReports := authResp.GetValue()
for {
nextPageUrl := authResp.GetOdataNextLink()
if nextPageUrl != nil {
authResp, err = client.Reports().AuthenticationMethods().UserRegistrationDetails().WithUrl(*nextPageUrl).Get(ctx, nil)
if err != nil {
log.Printf("error getting auth reports: %v", err)
continue
}
authReports = append(authReports, authResp.GetValue()...)
} else {
break
}
}
return authReports
Step 5: Assemble the Report
Now, we compile the retrieved data into a structured report. Skipping Guest
users:
Why Skip
Guest
Users? In many compliance and security audits, Guest accounts (external users invited to the directory) are often excluded because they typically have limited permissions and do not require the same level of scrutiny as >internal users. However, if your organization uses Guest accounts extensively for external collaboration, you might want to include them in the report and analyze their permissions separately.
for _, userData := range users {
userType := emptyIfNull(userData.GetUserType())
if userType == "Guest" {
continue
}
record := []string{
*userData.GetUserPrincipalName(),
*userData.GetDisplayName(),
}
user := findUser(usersAuthReport, userData.GetId())
if user != nil {
authReport := []string{
strconv.FormatBool(*user.GetIsAdmin()),
strconv.FormatBool(*user.GetIsMfaCapable()),
user.GetDefaultMfaMethod().String(),
strings.Join(user.GetMethodsRegistered(), "\n"),
strconv.FormatBool(*user.GetIsMfaRegistered()),
strconv.FormatBool(*user.GetIsPasswordlessCapable()),
strconv.FormatBool(*user.GetIsSsprCapable()),
strconv.FormatBool(*user.GetIsSsprEnabled()),
strconv.FormatBool(*user.GetIsSsprRegistered()),
strconv.FormatBool(*user.GetIsSystemPreferredAuthenticationMethodEnabled()),
user.GetLastUpdatedDateTime().String(),
}
record = append(record, authReport...)
} else {
for i := 0; i < 11; i++ {
record = append(record, "")
}
}
record = append(record, emptyIfNull(userData.GetMail()))
record = append(record, strconv.FormatBool(*userData.GetAccountEnabled()))
record = append(record, userType)
record = append(record, emptyIfNull(userData.GetJobTitle()))
record = append(record, emptyIfNull(userData.GetDepartment()))
record = append(record, emptyTimeIfNull(userData.GetCreatedDateTime()))
record = append(record, emptyTimeIfNull(userData.GetLastPasswordChangeDateTime()))
record = append(record, emptyIfNull(userData.GetPasswordPolicies()))
if userData.GetSignInActivity() != nil {
record = append(record, emptyTimeIfNull(userData.GetSignInActivity().GetLastSignInDateTime()))
record = append(record, emptyTimeIfNull(userData.GetSignInActivity().GetLastNonInteractiveSignInDateTime()))
} else {
record = append(record, "")
record = append(record, "")
}
record = append(record, strings.Join(findRoles(roles, userData.GetMemberOf()), "\n"))
if mailbox, err := client.Users().ByUserId(*userData.GetId()).MailboxSettings().Get(ctx, nil); err == nil {
record = append(record, mailbox.GetUserPurpose().String())
} else {
record = append(record, "")
}
if userData.GetOnPremisesSyncEnabled() != nil {
record = append(record, strconv.FormatBool(*userData.GetOnPremisesSyncEnabled()))
} else {
record = append(record, strconv.FormatBool(false))
}
records = append(records, record)
}
return records