{
  "schemaVersion": "1.0",
  "code": "70016",
  "name": "AuthorizationPending",
  "title": "AADSTS70016: Device code authorization is pending (fix)",
  "url": "https://brambrinkmeier.com/entra/aadsts/70016/",
  "alternates": {
    "markdown": "https://brambrinkmeier.com/entra/aadsts/70016.md",
    "json": "https://brambrinkmeier.com/api/aadsts/70016.json",
    "html": "https://brambrinkmeier.com/entra/aadsts/70016/"
  },
  "description": "AADSTS70016: a device code flow is waiting for the user to sign in. Normal, not a failure. Lab-measured code lifetime, KQL and the phishing angle.",
  "summary": "AADSTS70016 means a device code flow is waiting for the user to sign in. The app polls the token endpoint, nobody has entered the code yet, and Entra answers with 70016. It is the normal reply while a device code is outstanding, not a failure. For defenders it marks the window in which a device code phishing victim has been shown a code but has not used it.",
  "inMicrosoftReference": true,
  "unlistedChecked": null,
  "meaningText": "Microsoft's description:\n\n> **AuthorizationPending**: OAuth 2.0 device flow error. Authorization is pending. The device will retry polling the request.\n\nNothing is broken. 70016 is the answer the device code flow gives while it waits for a person.\n\nThe flow has two halves, described on Microsoft's [device authorization grant page](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code). First the app calls `/devicecode` and gets back a `device_code` (*\"A long string used to verify the session between the client and the authorization server\"*), a `user_code` (*\"A short string shown to the user used to identify the session on a secondary device\"*) and a `verification_uri` (*\"The URI the user should go to with the `user_code` in order to sign in\"*). Then, while the person signs in somewhere else, *\"the client should be polling the `/token` endpoint for the requested token using the `device_code`.\"*\n\nEvery poll that happens before the person has finished gets 70016. Microsoft's protocol-level name for the same condition is in its expected-errors table:\n\n| Error | Microsoft's description | Microsoft's client action |\n|---|---|---|\n| `authorization_pending` | The user hasn't finished authenticating, but hasn't canceled the flow. | Repeat the request after at least `interval` seconds. |\n\nSo the correct handling of 70016 is to wait and ask again. An app that treats it as a failure and stops has broken its own sign-in.\n\nCodes that do mean something went wrong sit next to it in Microsoft's reference:\n\n| Code | Microsoft's description | What happened |\n|---|---|---|\n| **70016** | *\"AuthorizationPending - OAuth 2.0 device flow error. Authorization is pending. The device will retry polling the request.\"* | Still waiting. Keep polling. |\n| **70018** | *\"BadVerificationCode - Invalid verification code due to User typing in wrong user code for device code flow. Authorization isn't approved.\"* | The person mistyped the short code. |\n| **70019** | *\"CodeExpired - Verification code expired. Have the user retry the sign-in.\"* | Time ran out. Start again. |\n| **90119** | *\"InvalidUserCode - The user code is null or empty.\"* | No code was submitted. |\n| **90120** | *\"InvalidDeviceFlowRequest - The request was already authorized or declined.\"* | The code was used once already. |\n| **65004** | *\"UserDeclinedConsent - User declined to consent to access the app.\"* | The person said no at the consent prompt. |\n| [7000014](https://brambrinkmeier.com/entra/aadsts/7000014/) | Not in Microsoft's reference. | The `device_code` value itself isn't recognised. |",
  "sections": [
    {
      "heading": "What does AADSTS70016 mean?",
      "text": "Microsoft's description:\n\n> **AuthorizationPending**: OAuth 2.0 device flow error. Authorization is pending. The device will retry polling the request.\n\nNothing is broken. 70016 is the answer the device code flow gives while it waits for a person.\n\nThe flow has two halves, described on Microsoft's [device authorization grant page](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code). First the app calls `/devicecode` and gets back a `device_code` (*\"A long string used to verify the session between the client and the authorization server\"*), a `user_code` (*\"A short string shown to the user used to identify the session on a secondary device\"*) and a `verification_uri` (*\"The URI the user should go to with the `user_code` in order to sign in\"*). Then, while the person signs in somewhere else, *\"the client should be polling the `/token` endpoint for the requested token using the `device_code`.\"*\n\nEvery poll that happens before the person has finished gets 70016. Microsoft's protocol-level name for the same condition is in its expected-errors table:\n\n| Error | Microsoft's description | Microsoft's client action |\n|---|---|---|\n| `authorization_pending` | The user hasn't finished authenticating, but hasn't canceled the flow. | Repeat the request after at least `interval` seconds. |\n\nSo the correct handling of 70016 is to wait and ask again. An app that treats it as a failure and stops has broken its own sign-in.\n\nCodes that do mean something went wrong sit next to it in Microsoft's reference:\n\n| Code | Microsoft's description | What happened |\n|---|---|---|\n| **70016** | *\"AuthorizationPending - OAuth 2.0 device flow error. Authorization is pending. The device will retry polling the request.\"* | Still waiting. Keep polling. |\n| **70018** | *\"BadVerificationCode - Invalid verification code due to User typing in wrong user code for device code flow. Authorization isn't approved.\"* | The person mistyped the short code. |\n| **70019** | *\"CodeExpired - Verification code expired. Have the user retry the sign-in.\"* | Time ran out. Start again. |\n| **90119** | *\"InvalidUserCode - The user code is null or empty.\"* | No code was submitted. |\n| **90120** | *\"InvalidDeviceFlowRequest - The request was already authorized or declined.\"* | The code was used once already. |\n| **65004** | *\"UserDeclinedConsent - User declined to consent to access the app.\"* | The person said no at the consent prompt. |\n| [7000014](https://brambrinkmeier.com/entra/aadsts/7000014/) | Not in Microsoft's reference. | The `device_code` value itself isn't recognised. |"
    },
    {
      "heading": "Reproduced in the lab",
      "text": "On 31 October 2026 an automated lab run requested a device code from a lab tenant and then polled the token endpoint before anyone had signed in. Entra returned:\n\n> AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling.\n\nNote the wording. The reference says *\"The device will retry polling the request\"*; the message the token endpoint actually returns says *\"Continue polling.\"* If you are matching on message text, match on the part both have.\n\nThe run then polled again immediately, with no wait. It got the same code and the same message, not a separate too-fast error. That is one observation from one tenant: it does not prove the endpoint never throttles a fast poller, only that an immediate second poll was answered the same way here.\n\n**Measured code lifetime.** The device code in this run was issued with `expires_in` of **900 seconds (15 minutes)** and an `interval` of **5 seconds**. Microsoft documents both fields (`expires_in`: *\"The number of seconds before the `device_code` and `user_code` expire\"*; `interval`: *\"The number of seconds the client should wait between polling requests\"*) and its page states the default: *\"From the moment the request is sent, the user has 15 minutes to sign in. This is the default value for `expires_in`.\"* The lab value matches that documented default. The `interval` of 5 seconds is not a number Microsoft publishes anywhere we have found, so treat 5 as what this tenant returned on 31 October 2026, not as a guaranteed constant: read `interval` from the response rather than hard-coding it.\n\nThat 15-minute window is also how long 70016 can keep coming back for a single code, which matters for the phishing case below.\n\nNo sign-in log row was captured for 70016. See [how pages are checked](https://brambrinkmeier.com/methodology/)."
    },
    {
      "heading": "Is it an attack?",
      "text": "The code on its own, no. It is the expected reply during a device code sign-in, and a healthy tenant that uses the flow produces it constantly.\n\nThe pattern can matter, because 70016 is exactly what device code phishing looks like while the lure is live. Microsoft's [Conditional Access authentication flows page](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows) is blunt about the flow: *\"Device code flow is a high-risk authentication method that can be part of a phishing attack or used to access corporate resources on unmanaged devices.\"*\n\nIn that attack the attacker, not the victim, runs the client. The attacker requests a device code, sends the `user_code` and the verification URL to the victim in a message that looks like a legitimate prompt, and polls. Each poll returns 70016 until the victim enters the code; then the attacker's poll returns tokens. So:\n\n- **70016 is the lure window.** It is the only part of the attack that is visible before a successful token issue, and it lasts at most the code lifetime measured above.\n- **A successful device code sign-in is the damage.** Hunt those, with [the token replay and session hijack guide](https://brambrinkmeier.com/guides/detect-token-replay-m365-session-hijack-entra-id/), which covers the device code filters in the sign-in logs and the Conditional Access control that blocks the flow.\n\n| Pattern | Likely meaning |\n|---|---|\n| Steady 70016 from a tool a user is actively signing into, ending in a success | Normal device code sign-in. |\n| 70016 for an app nobody in your tenant should be using device code with | Worth a look. Device code works with public client apps, so the client can be anything, anywhere. |\n| A device code success from an IP or country that never appears for that user, shortly after 70016 polling | The shape of a successful device code phish. Treat as a session compromise and revoke. |\n| 70016 and nothing else, repeatedly, for the same app | A code was shown and never entered, or a tool that polls and is never completed. Benign on its own. |\n| [7000014](https://brambrinkmeier.com/entra/aadsts/7000014/) instead of 70016 | A broken client, not a pending authorization. |\n\nOne honest limit: we did not capture a 70016 sign-in log row, so whether the attacker's polling requests appear in your tenant's logs at all is unconfirmed. The poll names a client and a `device_code`, but no user has approved anything yet, so there may be no user and no tenant row to attribute it to. Build detections on the device code **successes**, which definitely are logged, and use 70016 only as context."
    },
    {
      "heading": "How do I find it in my logs (KQL)?",
      "text": "Which sign-in table 70016 lands in is not documented, and the lab run produced no row to confirm it. Start with a union so you find out for your own tenant:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70016\", \"70018\", \"70019\", \"90119\", \"90120\")\n| summarize Rows = count(),\n            LastSeen = max(TimeGenerated),\n            Descriptions = make_set(ResultDescription, 5)\n          by SourceTable, ResultType\n| sort by Rows desc\n```\n\nIf that returns nothing, 70016 is not surfaced to your tenant, and the next query is the one that matters. `AuthenticationProtocol` is documented with *\"deviceCode\"* among its possible values, so you can inventory the flow itself rather than its pending state:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\" or OriginalTransferMethod == \"deviceCodeFlow\"\n| summarize Sign_ins = count(),\n            Successes = countif(ResultType == \"0\"),\n            Users = dcount(UserPrincipalName),\n            IPs = make_set(IPAddress, 10),\n            Codes = make_set(ResultType, 10)\n          by SourceTable, AppDisplayName, AppId\n| sort by Sign_ins desc\n```\n\nAn app in that list that you did not expect is worth a question whatever its result codes. The exact string stored in `OriginalTransferMethod` can differ between tenants, so confirm it on one known device code row before you rely on that half of the filter.\n\nThen look at the successes per user and per location, which is where a phish shows:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\"\n| where ResultType == \"0\"\n| summarize Sign_ins = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppDisplayName, 5),\n            IPs = make_set(IPAddress, 10)\n          by UserPrincipalName\n| sort by Sign_ins desc\n```\n\nAs a starting point, treat a user's **first ever** device code success as worth reading in full, and any device code success for a user who does not work with shared or input-constrained devices as suspicious. Tune from your own baseline; a tenant that uses the flow for conference room devices will have a large legitimate one."
    },
    {
      "heading": "How does Conditional Access affect it?",
      "text": "- Microsoft says to *\"Allow device code flow only where necessary\"* and that it *\"recommends blocking device code flow wherever possible\"*, using a Conditional Access authentication flows policy. Its own example is to allow the flow for conference room devices in one network location and block it everywhere else.\n- Our reading: a Conditional Access block does not change 70016. The policy is evaluated when the person signs in at the verification URL, so the polling client keeps getting 70016 until the code expires or the person's sign-in is blocked. That ordering is untested in the lab.\n- Blocking the flow stops the attacker's client as well as the user's, which is the point of blocking it."
    },
    {
      "heading": "How do I fix AADSTS70016?",
      "text": "Nothing is wrong. There is no fix, only correct handling.\n\n**For the user:** finish signing in. The device or tool shows a short code and a URL; open the URL, enter the code, and sign in. The tool completes by itself within a few seconds. If you were not expecting a code, and especially if someone sent you one in a message, do not enter it: that is how device code phishing works. Report it instead.\n\n**For developers:**\n1. Treat 70016 and `authorization_pending` as \"not yet\", not as an error. Keep polling.\n2. Wait at least `interval` seconds between polls, reading `interval` from the `/devicecode` response.\n3. Stop when `expires_in` has passed, and surface the expiry to the user so they can start again.\n4. Stop on the terminal errors instead of retrying: `authorization_declined`, `bad_verification_code` and `expired_token`.\n5. Use MSAL rather than hand-written polling. It implements the wait, the retry and the terminal cases for you.\n\n**For admins:**\n1. Inventory the flow with the second query. Decide which apps and which devices genuinely need it.\n2. If nothing needs it, block device code flow with a Conditional Access authentication flows policy. Start in report-only and exclude break-glass accounts.\n3. Tell users that a code arriving in a message or an email is a phishing attempt. The legitimate flow starts on a device in front of them.\n4. For a device code success you cannot explain, treat it as a compromised session: [revoke sessions and verify](https://brambrinkmeier.com/guides/revoke-sessions-and-verify-entra-id/)."
    }
  ],
  "microsoftWording": {
    "text": "**AuthorizationPending**: OAuth 2.0 device flow error. Authorization is pending. The device will retry polling the request.",
    "source": "Microsoft's error code reference, as quoted on the page",
    "sourceUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
  },
  "securityReading": {
    "category": {
      "id": "token",
      "label": "Tokens and sessions"
    },
    "kind": {
      "id": "interrupt",
      "label": "Interrupt (an expected extra step, not a failure)"
    },
    "attackSignal": {
      "level": "medium",
      "label": "Medium: worth a look in context"
    },
    "mitre": [],
    "tables": [],
    "isItAnAttack": "The code on its own, no. It is the expected reply during a device code sign-in, and a healthy tenant that uses the flow produces it constantly.\n\nThe pattern can matter, because 70016 is exactly what device code phishing looks like while the lure is live. Microsoft's [Conditional Access authentication flows page](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows) is blunt about the flow: *\"Device code flow is a high-risk authentication method that can be part of a phishing attack or used to access corporate resources on unmanaged devices.\"*\n\nIn that attack the attacker, not the victim, runs the client. The attacker requests a device code, sends the `user_code` and the verification URL to the victim in a message that looks like a legitimate prompt, and polls. Each poll returns 70016 until the victim enters the code; then the attacker's poll returns tokens. So:\n\n- **70016 is the lure window.** It is the only part of the attack that is visible before a successful token issue, and it lasts at most the code lifetime measured above.\n- **A successful device code sign-in is the damage.** Hunt those, with [the token replay and session hijack guide](https://brambrinkmeier.com/guides/detect-token-replay-m365-session-hijack-entra-id/), which covers the device code filters in the sign-in logs and the Conditional Access control that blocks the flow.\n\n| Pattern | Likely meaning |\n|---|---|\n| Steady 70016 from a tool a user is actively signing into, ending in a success | Normal device code sign-in. |\n| 70016 for an app nobody in your tenant should be using device code with | Worth a look. Device code works with public client apps, so the client can be anything, anywhere. |\n| A device code success from an IP or country that never appears for that user, shortly after 70016 polling | The shape of a successful device code phish. Treat as a session compromise and revoke. |\n| 70016 and nothing else, repeatedly, for the same app | A code was shown and never entered, or a tool that polls and is never completed. Benign on its own. |\n| [7000014](https://brambrinkmeier.com/entra/aadsts/7000014/) instead of 70016 | A broken client, not a pending authorization. |\n\nOne honest limit: we did not capture a 70016 sign-in log row, so whether the attacker's polling requests appear in your tenant's logs at all is unconfirmed. The poll names a client and a `device_code`, but no user has approved anything yet, so there may be no user and no tenant row to attribute it to. Build detections on the device code **successes**, which definitely are logged, and use 70016 only as context.",
    "conditionalAccess": {
      "heading": "How does Conditional Access affect it?",
      "text": "- Microsoft says to *\"Allow device code flow only where necessary\"* and that it *\"recommends blocking device code flow wherever possible\"*, using a Conditional Access authentication flows policy. Its own example is to allow the flow for conference room devices in one network location and block it everywhere else.\n- Our reading: a Conditional Access block does not change 70016. The policy is evaluated when the person signs in at the verification URL, so the polling client keeps getting 70016 until the code expires or the person's sign-in is blocked. That ordering is untested in the lab.\n- Blocking the flow stops the attacker's client as well as the user's, which is the point of blocking it."
    }
  },
  "kql": [
    {
      "title": null,
      "leadIn": "Which sign-in table 70016 lands in is not documented, and the lab run produced no row to confirm it. Start with a union so you find out for your own tenant:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70016\", \"70018\", \"70019\", \"90119\", \"90120\")\n| summarize Rows = count(),\n            LastSeen = max(TimeGenerated),\n            Descriptions = make_set(ResultDescription, 5)\n          by SourceTable, ResultType\n| sort by Rows desc"
    },
    {
      "title": null,
      "leadIn": "If that returns nothing, 70016 is not surfaced to your tenant, and the next query is the one that matters. `AuthenticationProtocol` is documented with *\"deviceCode\"* among its possible values, so you can inventory the flow itself rather than its pending state:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\" or OriginalTransferMethod == \"deviceCodeFlow\"\n| summarize Sign_ins = count(),\n            Successes = countif(ResultType == \"0\"),\n            Users = dcount(UserPrincipalName),\n            IPs = make_set(IPAddress, 10),\n            Codes = make_set(ResultType, 10)\n          by SourceTable, AppDisplayName, AppId\n| sort by Sign_ins desc"
    },
    {
      "title": null,
      "leadIn": "Then look at the successes per user and per location, which is where a phish shows:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\"\n| where ResultType == \"0\"\n| summarize Sign_ins = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppDisplayName, 5),\n            IPs = make_set(IPAddress, 10)\n          by UserPrincipalName\n| sort by Sign_ins desc"
    }
  ],
  "kqlSectionText": "Which sign-in table 70016 lands in is not documented, and the lab run produced no row to confirm it. Start with a union so you find out for your own tenant:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70016\", \"70018\", \"70019\", \"90119\", \"90120\")\n| summarize Rows = count(),\n            LastSeen = max(TimeGenerated),\n            Descriptions = make_set(ResultDescription, 5)\n          by SourceTable, ResultType\n| sort by Rows desc\n```\n\nIf that returns nothing, 70016 is not surfaced to your tenant, and the next query is the one that matters. `AuthenticationProtocol` is documented with *\"deviceCode\"* among its possible values, so you can inventory the flow itself rather than its pending state:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\" or OriginalTransferMethod == \"deviceCodeFlow\"\n| summarize Sign_ins = count(),\n            Successes = countif(ResultType == \"0\"),\n            Users = dcount(UserPrincipalName),\n            IPs = make_set(IPAddress, 10),\n            Codes = make_set(ResultType, 10)\n          by SourceTable, AppDisplayName, AppId\n| sort by Sign_ins desc\n```\n\nAn app in that list that you did not expect is worth a question whatever its result codes. The exact string stored in `OriginalTransferMethod` can differ between tenants, so confirm it on one known device code row before you rely on that half of the filter.\n\nThen look at the successes per user and per location, which is where a phish shows:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where AuthenticationProtocol == \"deviceCode\"\n| where ResultType == \"0\"\n| summarize Sign_ins = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppDisplayName, 5),\n            IPs = make_set(IPAddress, 10)\n          by UserPrincipalName\n| sort by Sign_ins desc\n```\n\nAs a starting point, treat a user's **first ever** device code success as worth reading in full, and any device code success for a user who does not work with shared or input-constrained devices as suspicious. Tune from your own baseline; a tenant that uses the flow for conference room devices will have a large legitimate one.",
  "fix": {
    "short": "Nothing to fix: 70016 is the expected reply while a device code is outstanding. User: finish signing in at the URL the device shows. Developer: keep polling `/token` no more often than every `interval` seconds and stop when the code expires. Admin: block device code flow if nobody needs it.",
    "steps": "Nothing is wrong. There is no fix, only correct handling.\n\n**For the user:** finish signing in. The device or tool shows a short code and a URL; open the URL, enter the code, and sign in. The tool completes by itself within a few seconds. If you were not expecting a code, and especially if someone sent you one in a message, do not enter it: that is how device code phishing works. Report it instead.\n\n**For developers:**\n1. Treat 70016 and `authorization_pending` as \"not yet\", not as an error. Keep polling.\n2. Wait at least `interval` seconds between polls, reading `interval` from the `/devicecode` response.\n3. Stop when `expires_in` has passed, and surface the expiry to the user so they can start again.\n4. Stop on the terminal errors instead of retrying: `authorization_declined`, `bad_verification_code` and `expired_token`.\n5. Use MSAL rather than hand-written polling. It implements the wait, the retry and the terminal cases for you.\n\n**For admins:**\n1. Inventory the flow with the second query. Decide which apps and which devices genuinely need it.\n2. If nothing needs it, block device code flow with a Conditional Access authentication flows policy. Start in report-only and exclude break-glass accounts.\n3. Tell users that a code arriving in a message or an email is a phishing attempt. The legitimate flow starts on a device in front of them.\n4. For a device code success you cannot explain, treat it as a compromised session: [revoke sessions and verify](https://brambrinkmeier.com/guides/revoke-sessions-and-verify-entra-id/)."
  },
  "labReproduced": {
    "date": "2026-10-31",
    "method": "Device code flow against a lab tenant",
    "scenario": "Device code issued but nobody has signed in yet",
    "message": "AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling.",
    "logCaptured": null,
    "kqlTested": null
  },
  "labSectionText": "On 31 October 2026 an automated lab run requested a device code from a lab tenant and then polled the token endpoint before anyone had signed in. Entra returned:\n\n> AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling.\n\nNote the wording. The reference says *\"The device will retry polling the request\"*; the message the token endpoint actually returns says *\"Continue polling.\"* If you are matching on message text, match on the part both have.\n\nThe run then polled again immediately, with no wait. It got the same code and the same message, not a separate too-fast error. That is one observation from one tenant: it does not prove the endpoint never throttles a fast poller, only that an immediate second poll was answered the same way here.\n\n**Measured code lifetime.** The device code in this run was issued with `expires_in` of **900 seconds (15 minutes)** and an `interval` of **5 seconds**. Microsoft documents both fields (`expires_in`: *\"The number of seconds before the `device_code` and `user_code` expire\"*; `interval`: *\"The number of seconds the client should wait between polling requests\"*) and its page states the default: *\"From the moment the request is sent, the user has 15 minutes to sign in. This is the default value for `expires_in`.\"* The lab value matches that documented default. The `interval` of 5 seconds is not a number Microsoft publishes anywhere we have found, so treat 5 as what this tenant returned on 31 October 2026, not as a guaranteed constant: read `interval` from the response rather than hard-coding it.\n\nThat 15-minute window is also how long 70016 can keep coming back for a single code, which matters for the phishing case below.\n\nNo sign-in log row was captured for 70016. See [how pages are checked](https://brambrinkmeier.com/methodology/).",
  "labRuns": [
    {
      "date": "2026-10-31",
      "run": "round 8",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/72",
      "scenario": "Device code issued but nobody has signed in yet",
      "expected": "70016",
      "got": "70016",
      "matchedExpectation": true,
      "message": "AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling."
    },
    {
      "date": "2026-10-31",
      "run": "round 8",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/72",
      "scenario": "Device code polled too fast",
      "expected": "70016 / 70020",
      "got": "70016",
      "matchedExpectation": true,
      "message": "AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling."
    },
    {
      "date": "2026-10-31",
      "run": "round 8",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/72",
      "scenario": "Device code redeemed with a different client ID",
      "expected": "70016 / 7000215 / 70002",
      "got": "90023",
      "matchedExpectation": false,
      "message": "AADSTS90023: ClientId passed in the request doesn't match the one in cache"
    },
    {
      "date": "2026-10-31",
      "run": "round 9",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/73",
      "scenario": "Device code issued but nobody has signed in yet",
      "expected": "70016",
      "got": "70016",
      "matchedExpectation": true,
      "message": "AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling."
    },
    {
      "date": "2026-10-31",
      "run": "round 9",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/73",
      "scenario": "Device code polled too fast",
      "expected": "70016 / 70020",
      "got": "70016",
      "matchedExpectation": true,
      "message": "AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling."
    },
    {
      "date": "2026-10-31",
      "run": "round 9",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/73",
      "scenario": "Device code redeemed with a different client ID",
      "expected": "70016 / 7000215 / 70002",
      "got": "90023",
      "matchedExpectation": false,
      "message": "AADSTS90023: ClientId passed in the request doesn't match the one in cache"
    }
  ],
  "verification": "lab-reproduced",
  "related": [
    "7000014",
    "90023",
    "50059",
    "90133"
  ],
  "coveredBy": [],
  "sources": [
    {
      "title": "Microsoft Entra authentication and authorization error codes",
      "url": "https://learn.microsoft.com/entra/identity-platform/reference-error-codes"
    },
    {
      "title": "Microsoft identity platform and the OAuth 2.0 device authorization grant flow",
      "url": "https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code"
    },
    {
      "title": "Conditional Access: Authentication flows",
      "url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows"
    },
    {
      "title": "SigninLogs table reference (Azure Monitor)",
      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs"
    }
  ],
  "published": "2026-10-31",
  "lastUpdated": "2026-10-31",
  "lastVerified": null,
  "license": "CC BY 4.0",
  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
  "licenseNote": "CC BY 4.0 covers the author's own text and classification fields. Quoted Microsoft error wording belongs to Microsoft, is quoted with its source and can change.",
  "attribution": "Bram Brinkmeier, \"AADSTS70016: Device code authorization is pending (fix)\", https://brambrinkmeier.com/entra/aadsts/70016/ (CC BY 4.0)",
  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=70016"
}
