Had to do something around this today and thought I’d churn out a quick blog post.
The list signIns endpoint can be used to get sign-in info. To copy from the MS documentation, a typical response looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#auditLogs/signIns", "@odata.nextLink": "https://graph.microsoft.com/v1.0/auditLogs/signIns?$top=1&$skiptoken=9177f2e3532fcd4c4d225f68f7b9bdf7_1", "value": [ { "id": "66ea54eb-6301-4ee5-be62-ff5a759b0100", "createdDateTime": "2020-03-13T19:15:41.6195833Z", "userDisplayName": "Test Contoso", "userPrincipalName": "testaccount1@contoso.com", "userId": "26be570a-ae82-4189-b4e2-a37c6808512d", "appId": "de8bc8b5-d9f9-48b1-a8ad-b748da725064", "appDisplayName": "Graph explorer", "ipAddress": "131.107.159.37", "clientAppUsed": "Browser", "correlationId": "d79f5bee-5860-4832-928f-3133e22ae912", "conditionalAccessStatus": "notApplied", "isInteractive": true, "riskDetail": "none", "riskLevelAggregated": "none", "riskLevelDuringSignIn": "none", "riskState": "none", "riskEventTypes": [], "resourceDisplayName": "Microsoft Graph", "resourceId": "00000003-0000-0000-c000-000000000000", "status": { "errorCode": 0, "failureReason": null, "additionalDetails": null }, "deviceDetail": { "deviceId": "", "displayName": null, "operatingSystem": "Windows 10", "browser": "Edge 80.0.361", "isCompliant": null, "isManaged": null, "trustType": null }, "location": { "city": "Redmond", "state": "Washington", "countryOrRegion": "US", "geoCoordinates": { "altitude": null, "latitude": 47.68050003051758, "longitude": -122.12094116210938 } }, "appliedConditionalAccessPolicies": [ { "id": "de7e60eb-ed89-4d73-8205-2227def6b7c9", "displayName": "SharePoint limited access for guest workers", "enforcedGrantControls": [], "enforcedSessionControls": [], "result": "notEnabled" }, { "id": "6701123a-b4c6-48af-8565-565c8bf7cabc", "displayName": "Medium signin risk block", "enforcedGrantControls": [], "enforcedSessionControls": [], "result": "notEnabled" }, ] } ] } |
Above, there’s only one record, but by default the endpoint returns 1000 records. And they are sorted in descending order of the createdDateTime value.
So if you just want the sign-in info of one user it’s best to filter on that.
1 |
'https://graph.microsoft.com/v1.0/auditLogs/signIns?&$filter=userPrincipalName eq ''me@mydomain.com''' |
This will a bunch of recent sign-ins. In my case, for instance, I got about 400+ records. Obviously I don’t want all that if I am looking for my lates sign-in. In such cases, best to get only the top most record.
1 |
'https://graph.microsoft.com/v1.0/auditLogs/signIns?&$filter=userPrincipalName eq ''me@mydomain.com''&$top=1' |
If using the PowerShell module, the equivalent is:
1 |
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'me@mydomain.com'" -Top 1 |
It’s also possible to filter by the clients too. The signIn
record has a clientAppUsed property. So, I can do:
1 |
'https://graph.microsoft.com/v1.0/auditLogs/signIns?&$filter=userPrincipalName eq ''me@mydomain.com'' and clientAppUsed eq ''Mobile Apps and Desktop clients''&$top=1' |
Where possible it is better to filter at the source (Graph) level rather than get all records and then filter them.
Bear in mind, these records are for both successful and unsuccessful sign-ins. Sometimes the top record would have a status.additionalDetails
value of “The user didn’t enter the right credentials. It’s expected to see some number of these errors in your logs due to users making mistakes.” – meaning, it wasn’t successful, and so is pretty useless if all I am looking for is the last successful sign-in.
The errorCode
property within the status
property can be used to filter on successful sign-ins. When successful the value is 0. So one can modify the above query to get the last successful sign-in of a user thus:
1 |
'https://graph.microsoft.com/v1.0/auditLogs/signIns?&$filter=userPrincipalName eq ''me@mydomain.com'' and status/errorCode eq 0&$top=1' |
I would thought I should use the any
operator to filter on errorCode
but was wrong. It only supports eq
as per the documentation. The documentation lists more properties that can be filtered upon.