|
| 1 | +""" |
| 2 | +Authentication methods — audit registered methods per user and check |
| 3 | +tenant-wide passwordless / MFA / SSPR readiness. |
| 4 | +
|
| 5 | +Shows three perspectives: |
| 6 | + 1. Tenant-wide summary: users registered by method and by feature. |
| 7 | + 2. Per-user detail: which methods each user has registered and what |
| 8 | + they're capable of (MFA, passwordless, SSPR). |
| 9 | + 3. Per-user authentication methods: FIDO2 keys, Microsoft |
| 10 | + Authenticator, phone, password. |
| 11 | +
|
| 12 | +Useful for MFA roll-out tracking, passwordless adoption reporting, |
| 13 | +and security compliance audits. |
| 14 | +
|
| 15 | +Requires delegated permission ``UserAuthenticationMethod.Read.All`` |
| 16 | +and ``AuditLog.Read.All``. |
| 17 | +
|
| 18 | +https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethods-overview |
| 19 | +""" |
| 20 | + |
| 21 | +from office365.graph_client import GraphClient |
| 22 | +from tests import test_client_id, test_client_secret, test_tenant |
| 23 | + |
| 24 | + |
| 25 | +def main(): |
| 26 | + client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret) |
| 27 | + |
| 28 | + am = client.reports.authentication_methods |
| 29 | + |
| 30 | + # -- Step 1: tenant-wide summary by feature (MFA, SSPR, passwordless) -- |
| 31 | + feature_summary = am.users_registered_by_feature().execute_query() |
| 32 | + if feature_summary and feature_summary.value: |
| 33 | + fs = feature_summary.value |
| 34 | + print("Users by authentication feature:") |
| 35 | + print(f" Total users : {fs.total_user_count}") |
| 36 | + print(f" MFA registered : {fs.mfa_registered_count or '?'}") |
| 37 | + print(f" SSPR capable : {fs.sspr_capable_count or '?'}") |
| 38 | + print(f" SSPR enabled : {fs.sspr_enabled_count or '?'}") |
| 39 | + print(f" Passwordless capable: {fs.passwordless_capable_count or '?'}") |
| 40 | + print() |
| 41 | + |
| 42 | + # -- Step 2: tenant-wide summary by method -- |
| 43 | + method_summary = am.users_registered_by_method().execute_query() |
| 44 | + if method_summary and method_summary.value: |
| 45 | + ms = method_summary.value |
| 46 | + print("Users by authentication method:") |
| 47 | + if hasattr(ms, "users_registered_by_method") and ms.users_registered_by_method: |
| 48 | + for m in ms.users_registered_by_method: |
| 49 | + print(f" {m.method_type or '?':30s} {m.user_count or 0} users") |
| 50 | + elif hasattr(ms, "registration_count") and ms.registration_count: |
| 51 | + for rc in ms.registration_count: |
| 52 | + print(f" {rc.method or '?':30s} {rc.count or 0} users") |
| 53 | + print() |
| 54 | + |
| 55 | + # -- Step 3: per-user registration details -- |
| 56 | + details = am.user_registration_details.top(20).get().execute_query() |
| 57 | + print(f"User registration details (showing {len(details)} users):\n") |
| 58 | + print(f"{'UPN':35s} {'MFA':6s} {'PW-less':9s} {'SSPR-cap':9s} {'SSPR-en':8s} {'Admin':6s} {'Preferred method'}") |
| 59 | + print("-" * 100) |
| 60 | + |
| 61 | + for d in details: |
| 62 | + pref = d.user_preferred_method_for_secondary_authentication or "-" |
| 63 | + print(f"{d.user_principal_name[:33]:35s} " |
| 64 | + f"{'✔' if d.is_mfa_registered else '✗':6s} " |
| 65 | + f"{'✔' if d.is_passwordless_capable else '✗':9s} " |
| 66 | + f"{'✔' if d.is_sspr_capable else '✗':9s} " |
| 67 | + f"{'✔' if d.is_sspr_enabled else '✗':8s} " |
| 68 | + f"{'✔' if d.is_admin else '':6s} " |
| 69 | + f"{pref}") |
| 70 | + |
| 71 | + # -- Step 4: per-user registered methods for a specific user -- |
| 72 | + print() |
| 73 | + sample_upn = details[0].user_principal_name if details else "admin@contoso.onmicrosoft.com" |
| 74 | + users = client.users.filter(f"userPrincipalName eq '{sample_upn}'").get().execute_query() |
| 75 | + if users: |
| 76 | + user = users[0] |
| 77 | + auth = user.authentication |
| 78 | + all_methods = auth.methods.get().execute_query() |
| 79 | + print(f"Authentication methods for {user.user_principal_name}: {len(all_methods)}") |
| 80 | + for m in all_methods: |
| 81 | + mtype = m.properties.get("@odata.type", m.entity_type_name) |
| 82 | + extra = "" |
| 83 | + if "phone" in mtype.lower(): |
| 84 | + extra = m.properties.get("phoneNumber", "") or m.properties.get("phoneType", "") |
| 85 | + elif "fido" in mtype.lower(): |
| 86 | + extra = m.properties.get("displayName", "") |
| 87 | + elif "microsoftAuthenticator" in mtype.lower(): |
| 88 | + extra = m.properties.get("displayName", "") |
| 89 | + print(f" {mtype:55s} {extra}") |
| 90 | + |
| 91 | + |
| 92 | +if __name__ == "__main__": |
| 93 | + main() |
0 commit comments