{
  "openapi": "3.1.0",
  "info": {
    "title": "Bram Brinkmeier: Entra ID sign-in error data",
    "version": "1.0.0",
    "summary": "Static, read-only JSON, Markdown and text files about Microsoft Entra ID AADSTS sign-in error codes and Microsoft Graph error codes.",
    "description": "Every path is a static file generated at build time from the site's content (118 AADSTS code pages, 35 lab-reproduced; 14 lab runs). No authentication, no query parameters, no server-side logic. Identifiers from the lab tenant are replaced with placeholders such as {tenant-id}. Generated 2026-11-08T15:15:25.046Z. Human-readable description of the fields, freshness and corrections: https://brambrinkmeier.com/for-agents/.",
    "contact": {
      "name": "Bram Brinkmeier",
      "url": "https://brambrinkmeier.com/about/"
    },
    "license": {
      "name": "CC BY 4.0",
      "identifier": "CC-BY-4.0"
    }
  },
  "externalDocs": {
    "description": "For AI agents and tools: what exists, field meanings, freshness, how to cite",
    "url": "https://brambrinkmeier.com/for-agents/"
  },
  "servers": [
    {
      "url": "https://brambrinkmeier.com"
    }
  ],
  "paths": {
    "/api/aadsts/index.json": {
      "get": {
        "operationId": "listAadstsCodes",
        "summary": "Every published AADSTS code page with its HTML, Markdown and JSON URLs",
        "responses": {
          "200": {
            "description": "Index of code pages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodeIndex"
                },
                "example": {
                  "schemaVersion": "1.0",
                  "generatedAt": "2026-11-08T15:15:25.046Z",
                  "name": "AADSTS code pages on brambrinkmeier.com",
                  "description": "One entry per published AADSTS code page, with the links to its HTML page, its Markdown twin and its JSON record. labReproduced is true only when the page records a lab reproduction (date and scenario in the JSON record).",
                  "license": "CC BY 4.0",
                  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
                  "attribution": "Bram Brinkmeier, https://brambrinkmeier.com/ (Bram Brinkmeier), CC BY 4.0",
                  "documentation": "https://brambrinkmeier.com/for-agents/",
                  "count": 118,
                  "labReproducedCount": 35,
                  "codes": [
                    {
                      "code": "16000",
                      "title": "AADSTS16000: Account isn't in the tenant the app uses (fix)",
                      "url": "https://brambrinkmeier.com/entra/aadsts/16000/",
                      "json": "https://brambrinkmeier.com/api/aadsts/16000.json",
                      "markdown": "https://brambrinkmeier.com/entra/aadsts/16000.md",
                      "labReproduced": false,
                      "inMicrosoftReference": true,
                      "attackSignal": "low",
                      "lastUpdated": "2026-10-30"
                    },
                    {
                      "code": "50001",
                      "title": "AADSTS50001 fix: resource disabled or doesn't exist",
                      "url": "https://brambrinkmeier.com/entra/aadsts/50001/",
                      "json": "https://brambrinkmeier.com/api/aadsts/50001.json",
                      "markdown": "https://brambrinkmeier.com/entra/aadsts/50001.md",
                      "labReproduced": false,
                      "inMicrosoftReference": true,
                      "attackSignal": "low",
                      "lastUpdated": "2026-11-01"
                    },
                    {
                      "code": "50011",
                      "title": "AADSTS50011: Reply URL does not match",
                      "url": "https://brambrinkmeier.com/entra/aadsts/50011/",
                      "json": "https://brambrinkmeier.com/api/aadsts/50011.json",
                      "markdown": "https://brambrinkmeier.com/entra/aadsts/50011.md",
                      "labReproduced": false,
                      "inMicrosoftReference": true,
                      "attackSignal": "low",
                      "lastUpdated": "2026-10-11"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/aadsts/{code}.json": {
      "get": {
        "operationId": "getAadstsCode",
        "summary": "Full record of one AADSTS code page",
        "description": "Same facts as the HTML page at /entra/aadsts/{code}/ and the Markdown twin at /entra/aadsts/{code}.md, built from the same source at the same build.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "AADSTS code without the AADSTS prefix, as listed in /api/aadsts/index.json",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4,7}$"
            },
            "example": "50027"
          }
        ],
        "responses": {
          "200": {
            "description": "Code record",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodeRecord"
                },
                "example": {
                  "schemaVersion": "1.0",
                  "code": "50027",
                  "name": "InvalidJwtToken",
                  "title": "AADSTS50027: JWT token is invalid or malformed (fix)",
                  "url": "https://brambrinkmeier.com/entra/aadsts/50027/",
                  "alternates": {
                    "markdown": "https://brambrinkmeier.com/entra/aadsts/50027.md",
                    "json": "https://brambrinkmeier.com/api/aadsts/50027.json",
                    "html": "https://brambrinkmeier.com/entra/aadsts/50027/"
                  },
                  "description": "AADSTS50027: Entra rejected a JWT as invalid or malformed. Client assertions and federated ID tokens are the usual sources. Fix and KQL.",
                  "summary": "AADSTS50027 means Microsoft Entra ID received a JWT it would not accept: wrong format, missing or duplicated claims, unexpected issuer or audience, outside its valid time range, or a failed signature check on an external ID token. It is usually a developer error. The two common sources are a hand-built client assertion and a federated identity provider's ID token.",
                  "inMicrosoftReference": true,
                  "unlistedChecked": null,
                  "meaningText": "Microsoft's description:\n\n> **InvalidJwtToken**: Invalid JWT token because of the following reasons:\n> - doesn't contain nonce claim, sub claim\n> - subject identifier mismatch\n> - duplicate claim in idToken claims\n> - unexpected issuer\n> - unexpected audience\n> - not within its valid time range\n> - token format isn't proper\n> - External ID token from issuer failed signature verification.\n\nThat is eight different conditions behind one code, which is why the fix depends entirely on which JWT Entra was looking at. Two kinds of request put a JWT in front of Entra:\n\n- **A client assertion.** An app authenticating with a certificate or a federated credential sends `client_assertion` instead of a secret. Our lab hit 50027 this way.\n- **An external ID token.** A federated identity provider returns an ID token to Entra, which validates it. The last bullet in Microsoft's list, *\"External ID token from issuer failed signature verification\"*, is this case, and so are the `nonce`, `sub` and duplicate-claim bullets, which name `idToken` explicitly.\n\nFor the client assertion case, Microsoft's certificate credentials page documents what the JWT must contain. The header:\n\n| Parameter | Microsoft's remark |\n|---|---|\n| `alg` | Should be **PS256** |\n| `typ` | Should be **JWT** |\n| `x5t#S256` | Base64url-encoded SHA-256 thumbprint of the X.509 certificate's DER encoding. |\n\nAnd the claims: `aud` (`https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token`), `iss` and `sub` (both the client ID), `jti`, `nbf`, `exp` and `iat`. On the lifetime, Microsoft says to *\"keep it short - 5-10 minutes after `nbf` at most\"*. The signature *\"is computed by applying the certificate as described in the JSON Web Token RFC7519 specification\"*, using PSS padding.\n\nSeveral of Microsoft's eight bullets map onto that table: `aud` wrong is \"unexpected audience\", `iss` wrong is \"unexpected issuer\", and an `exp` in the past is \"not within its valid time range\".\n\n### Which assertion error you get, and in what order\nOur lab separated three of the assertion failures. Entra checks them in this order:\n\n| What is wrong | Code |\n|---|---|\n| `client_assertion_type` is not the JWT bearer URN | [90023](https://brambrinkmeier.com/entra/aadsts/90023/) *\"Unexpected client assertion type.\"* |\n| The assertion is not a well-formed JWT | **50027** |\n| The assertion is a JWT but its validity window has passed or not begun | [700024](https://brambrinkmeier.com/entra/aadsts/700024/) |\n| Nothing was sent at all on a `client_credentials` request | [7000216](https://brambrinkmeier.com/entra/aadsts/7000216/) |\n\nMicrosoft's reference also lists **AADSTS700027**, *\"Client assertion failed signature validation\"*, for a well-formed JWT with a bad signature. So 50027 sits between \"wrong kind of parameter\" and \"right shape, wrong signature\": the string reached the JWT parser and the parser rejected it.",
                  "sections": [
                    {
                      "heading": "What does AADSTS50027 mean?",
                      "text": "Microsoft's description:\n\n> **InvalidJwtToken**: Invalid JWT token because of the following reasons:\n> - doesn't contain nonce claim, sub claim\n> - subject identifier mismatch\n> - duplicate claim in idToken claims\n> - unexpected issuer\n> - unexpected audience\n> - not within its valid time range\n> - token format isn't proper\n> - External ID token from issuer failed signature verification.\n\nThat is eight different conditions behind one code, which is why the fix depends entirely on which JWT Entra was looking at. Two kinds of request put a JWT in front of Entra:\n\n- **A client assertion.** An app authenticating with a certificate or a federated credential sends `client_assertion` instead of a secret. Our lab hit 50027 this way.\n- **An external ID token.** A federated identity provider returns an ID token to Entra, which validates it. The last bullet in Microsoft's list, *\"External ID token from issuer failed signature verification\"*, is this case, and so are the `nonce`, `sub` and duplicate-claim bullets, which name `idToken` explicitly.\n\nFor the client assertion case, Microsoft's certificate credentials page documents what the JWT must contain. The header:\n\n| Parameter | Microsoft's remark |\n|---|---|\n| `alg` | Should be **PS256** |\n| `typ` | Should be **JWT** |\n| `x5t#S256` | Base64url-encoded SHA-256 thumbprint of the X.509 certificate's DER encoding. |\n\nAnd the claims: `aud` (`https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token`), `iss` and `sub` (both the client ID), `jti`, `nbf`, `exp` and `iat`. On the lifetime, Microsoft says to *\"keep it short - 5-10 minutes after `nbf` at most\"*. The signature *\"is computed by applying the certificate as described in the JSON Web Token RFC7519 specification\"*, using PSS padding.\n\nSeveral of Microsoft's eight bullets map onto that table: `aud` wrong is \"unexpected audience\", `iss` wrong is \"unexpected issuer\", and an `exp` in the past is \"not within its valid time range\".\n\n### Which assertion error you get, and in what order\nOur lab separated three of the assertion failures. Entra checks them in this order:\n\n| What is wrong | Code |\n|---|---|\n| `client_assertion_type` is not the JWT bearer URN | [90023](https://brambrinkmeier.com/entra/aadsts/90023/) *\"Unexpected client assertion type.\"* |\n| The assertion is not a well-formed JWT | **50027** |\n| The assertion is a JWT but its validity window has passed or not begun | [700024](https://brambrinkmeier.com/entra/aadsts/700024/) |\n| Nothing was sent at all on a `client_credentials` request | [7000216](https://brambrinkmeier.com/entra/aadsts/7000216/) |\n\nMicrosoft's reference also lists **AADSTS700027**, *\"Client assertion failed signature validation\"*, for a well-formed JWT with a bad signature. So 50027 sits between \"wrong kind of parameter\" and \"right shape, wrong signature\": the string reached the JWT parser and the parser rejected it."
                    },
                    {
                      "heading": "Reproduced in the lab",
                      "text": "On 30 October 2026 an automated lab run sent a client-credentials token request with the correct `client_assertion_type` and a `client_assertion` that was not a JWT. Entra returned:\n\n> AADSTS50027: JWT token is invalid or malformed.\n\nBoth rounds of the run returned the same code and the same one-line message, short enough that the lab's 160-character cut does not truncate it. The run had expected 9002313, 50027 or 700027, and 50027 is the one it got. In the same run, the same request with an unaccepted `client_assertion_type` returned [90023](https://brambrinkmeier.com/entra/aadsts/90023/), and a syntactically valid assertion whose validity window had passed returned [700024](https://brambrinkmeier.com/entra/aadsts/700024/) with the current time and the assertion's `nbf` in the message. So the lab confirms the format case specifically, and not the other seven conditions in Microsoft's list.\n\nNo sign-in log row was captured. See [how pages are checked](https://brambrinkmeier.com/methodology/)."
                    },
                    {
                      "heading": "Is it an attack?",
                      "text": "Usually not, and this is the one code on this page's family where the answer deserves a second look. A malformed JWT is what a developer produces; it is also what someone replaying or editing a captured token produces.\n\n| Pattern | Likely meaning |\n|---|---|\n| Repeated 50027 for one app from your own build agents or servers | A client assertion implementation under development, or a certificate rotation that went wrong. Benign. |\n| 50027 on an app that has worked for months, with no deployment | Look at the certificate. An expired or replaced certificate usually gives a different code, but a half-finished rotation can produce assertions nobody can parse. |\n| 50027 for one app from an address you don't own | Someone is sending assertions for your app ID. They do not have a working credential, or you would see a success instead. Check for successes from the same address. |\n| 50027 on user sign-ins through a federated identity provider | The identity provider's ID token is being rejected. Check the issuer, the audience and the signing key on the provider, and whether a certificate was rolled. |\n| A burst across several apps from one address | Someone is probing with crafted tokens. Nothing is disclosed by this error, but the source is worth a look. |\n\nOur reading on the signal: 50027 means a token was refused, so it is evidence of a failure rather than of a compromise. What makes it worth watching is a success from the same source shortly after."
                    },
                    {
                      "heading": "How do I find it in my logs (KQL)?",
                      "text": "Which table 50027 lands in depends on which JWT failed, and Microsoft doesn't document it for either case. Our reading: a client assertion is an app signing in, so `AADServicePrincipalSignInLogs`; a federated user's ID token is a user sign-in, so `SigninLogs` or `AADNonInteractiveUserSignInLogs`. Start with the union:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Rows = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppId, 10)\n          by SourceTable\n```\n\nThe filter covers both columns because the tables describe them differently: in `SigninLogs`, `ResultType` *\"provides the 5-6 digit error code that's generated during a sign-in event\"*, while `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"* and `ResultSignature` as *\"the error code, if any, for the sign-in operation\"*.\n\nFor the client assertion case, which app and which source:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Failures = count(),\n            LastSeen = max(TimeGenerated),\n            CredentialTypes = make_set(ClientCredentialType, 5),\n            Thumbprints = make_set(ServicePrincipalCredentialThumbprint, 5),\n            Descriptions = make_set(ResultDescription, 5)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Failures desc\n```\n\n`ClientCredentialType` is documented as *\"the type of client credential used. Examples include client assertion, client secret, etc.\"* Whether it and the thumbprint column are filled in when the JWT could not be parsed is not documented: a token Entra cannot read may not identify a credential at all. Check a row before you rely on either column.\n\nThe pattern worth alerting on is a refusal followed by a success from the same place:\n\n```kusto\nlet failedFrom =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(7d)\n    | where ResultType == \"50027\" or ResultSignature == \"50027\"\n    | summarize FirstFail = min(TimeGenerated) by AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"0\" or ResultType =~ \"Success\"\n| join kind=inner failedFrom on AppId, IPAddress\n| where TimeGenerated > FirstFail\n| project TimeGenerated, AppId, ServicePrincipalName, IPAddress, ClientCredentialType, FirstFail\n| sort by TimeGenerated asc\n```\n\nThresholds are a starting point only: for a developer's own address, dozens an hour mean nothing; for an address with no history for that app, one row is enough to look at."
                    },
                    {
                      "heading": "How does Conditional Access affect it?",
                      "text": "Not relevant to the client assertion case: there is no user and no policy evaluation, and the request fails on the credential. For the federated user case, 50027 happens while Entra is validating the incoming token, which is before the point where Conditional Access decides anything. Our reading, by analogy with other first-factor failures; Microsoft doesn't state it for this code."
                    },
                    {
                      "heading": "How do I fix AADSTS50027?",
                      "text": "**For developers sending a client assertion:**\n1. Decode what you are sending. If it isn't three base64url segments separated by dots, it is not a JWT and nothing else matters.\n2. Build the header with `alg` PS256, `typ` JWT and `x5t#S256` set to the certificate's SHA-256 thumbprint, and the claims `aud`, `iss`, `sub`, `jti`, `nbf`, `exp` and `iat` as Microsoft's certificate credentials page specifies. `iss` and `sub` are both the client ID.\n3. Keep the lifetime short, 5 to 10 minutes after `nbf`. If the window has passed you get [700024](https://brambrinkmeier.com/entra/aadsts/700024/) rather than 50027.\n4. Sign with the private key of a certificate registered on the app registration, using PSS padding.\n5. Let a library do it. MSAL builds the assertion from a certificate for you, and most 50027 in this case comes from hand-rolled code.\n6. Send it with `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, or you get [90023](https://brambrinkmeier.com/entra/aadsts/90023/) first.\n\n**For admins of a federated identity provider:**\n1. Work out which of Microsoft's eight conditions applies before changing anything. The `nonce`, `sub`, duplicate-claim and signature-verification bullets are all about an external ID token.\n2. Check the issuer and audience values configured on both sides, and that the provider's current signing key is the one Entra is reading. A key rollover on the provider gives *\"External ID token from issuer failed signature verification\"*.\n3. Check clock skew. *\"Not within its valid time range\"* is a time problem on the provider, not in Entra.\n\n**For admins generally:**\n1. Use the queries above to identify the app and its owner, and whether any source that failed also succeeded.\n2. If an address with no history for the app is sending assertions, treat the app ID as known to someone outside and review the app's permissions."
                    }
                  ],
                  "microsoftWording": {
                    "text": "**InvalidJwtToken**: Invalid JWT token because of the following reasons:\n- doesn't contain nonce claim, sub claim\n- subject identifier mismatch\n- duplicate claim in idToken claims\n- unexpected issuer\n- unexpected audience\n- not within its valid time range\n- token format isn't proper\n- External ID token from issuer failed signature verification.",
                    "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": "credentials",
                      "label": "Credentials and lockout"
                    },
                    "kind": {
                      "id": "failure",
                      "label": "Failure"
                    },
                    "attackSignal": {
                      "level": "medium",
                      "label": "Medium: worth a look in context"
                    },
                    "mitre": [],
                    "tables": [
                      "SigninLogs",
                      "AADServicePrincipalSignInLogs"
                    ],
                    "isItAnAttack": "Usually not, and this is the one code on this page's family where the answer deserves a second look. A malformed JWT is what a developer produces; it is also what someone replaying or editing a captured token produces.\n\n| Pattern | Likely meaning |\n|---|---|\n| Repeated 50027 for one app from your own build agents or servers | A client assertion implementation under development, or a certificate rotation that went wrong. Benign. |\n| 50027 on an app that has worked for months, with no deployment | Look at the certificate. An expired or replaced certificate usually gives a different code, but a half-finished rotation can produce assertions nobody can parse. |\n| 50027 for one app from an address you don't own | Someone is sending assertions for your app ID. They do not have a working credential, or you would see a success instead. Check for successes from the same address. |\n| 50027 on user sign-ins through a federated identity provider | The identity provider's ID token is being rejected. Check the issuer, the audience and the signing key on the provider, and whether a certificate was rolled. |\n| A burst across several apps from one address | Someone is probing with crafted tokens. Nothing is disclosed by this error, but the source is worth a look. |\n\nOur reading on the signal: 50027 means a token was refused, so it is evidence of a failure rather than of a compromise. What makes it worth watching is a success from the same source shortly after.",
                    "conditionalAccess": {
                      "heading": "How does Conditional Access affect it?",
                      "text": "Not relevant to the client assertion case: there is no user and no policy evaluation, and the request fails on the credential. For the federated user case, 50027 happens while Entra is validating the incoming token, which is before the point where Conditional Access decides anything. Our reading, by analogy with other first-factor failures; Microsoft doesn't state it for this code."
                    }
                  },
                  "kql": [
                    {
                      "title": null,
                      "leadIn": "Which table 50027 lands in depends on which JWT failed, and Microsoft doesn't document it for either case. Our reading: a client assertion is an app signing in, so `AADServicePrincipalSignInLogs`; a federated user's ID token is a user sign-in, so `SigninLogs` or `AADNonInteractiveUserSignInLogs`. Start with the union:",
                      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Rows = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppId, 10)\n          by SourceTable"
                    },
                    {
                      "title": null,
                      "leadIn": "For the client assertion case, which app and which source:",
                      "query": "AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Failures = count(),\n            LastSeen = max(TimeGenerated),\n            CredentialTypes = make_set(ClientCredentialType, 5),\n            Thumbprints = make_set(ServicePrincipalCredentialThumbprint, 5),\n            Descriptions = make_set(ResultDescription, 5)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Failures desc"
                    },
                    {
                      "title": null,
                      "leadIn": "The pattern worth alerting on is a refusal followed by a success from the same place:",
                      "query": "let failedFrom =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(7d)\n    | where ResultType == \"50027\" or ResultSignature == \"50027\"\n    | summarize FirstFail = min(TimeGenerated) by AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"0\" or ResultType =~ \"Success\"\n| join kind=inner failedFrom on AppId, IPAddress\n| where TimeGenerated > FirstFail\n| project TimeGenerated, AppId, ServicePrincipalName, IPAddress, ClientCredentialType, FirstFail\n| sort by TimeGenerated asc"
                    }
                  ],
                  "kqlSectionText": "Which table 50027 lands in depends on which JWT failed, and Microsoft doesn't document it for either case. Our reading: a client assertion is an app signing in, so `AADServicePrincipalSignInLogs`; a federated user's ID token is a user sign-in, so `SigninLogs` or `AADNonInteractiveUserSignInLogs`. Start with the union:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Rows = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            Apps = make_set(AppId, 10)\n          by SourceTable\n```\n\nThe filter covers both columns because the tables describe them differently: in `SigninLogs`, `ResultType` *\"provides the 5-6 digit error code that's generated during a sign-in event\"*, while `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"* and `ResultSignature` as *\"the error code, if any, for the sign-in operation\"*.\n\nFor the client assertion case, which app and which source:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"50027\" or ResultSignature == \"50027\"\n| summarize Failures = count(),\n            LastSeen = max(TimeGenerated),\n            CredentialTypes = make_set(ClientCredentialType, 5),\n            Thumbprints = make_set(ServicePrincipalCredentialThumbprint, 5),\n            Descriptions = make_set(ResultDescription, 5)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Failures desc\n```\n\n`ClientCredentialType` is documented as *\"the type of client credential used. Examples include client assertion, client secret, etc.\"* Whether it and the thumbprint column are filled in when the JWT could not be parsed is not documented: a token Entra cannot read may not identify a credential at all. Check a row before you rely on either column.\n\nThe pattern worth alerting on is a refusal followed by a success from the same place:\n\n```kusto\nlet failedFrom =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(7d)\n    | where ResultType == \"50027\" or ResultSignature == \"50027\"\n    | summarize FirstFail = min(TimeGenerated) by AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"0\" or ResultType =~ \"Success\"\n| join kind=inner failedFrom on AppId, IPAddress\n| where TimeGenerated > FirstFail\n| project TimeGenerated, AppId, ServicePrincipalName, IPAddress, ClientCredentialType, FirstFail\n| sort by TimeGenerated asc\n```\n\nThresholds are a starting point only: for a developer's own address, dozens an hour mean nothing; for an address with no history for that app, one row is enough to look at.",
                  "fix": {
                    "short": "Developer sending a client assertion: build a real JWT with the header and claims Microsoft documents, signed with the certificate registered on the app. Federation admin: fix the issuer, audience or signing key on the identity provider.",
                    "steps": "**For developers sending a client assertion:**\n1. Decode what you are sending. If it isn't three base64url segments separated by dots, it is not a JWT and nothing else matters.\n2. Build the header with `alg` PS256, `typ` JWT and `x5t#S256` set to the certificate's SHA-256 thumbprint, and the claims `aud`, `iss`, `sub`, `jti`, `nbf`, `exp` and `iat` as Microsoft's certificate credentials page specifies. `iss` and `sub` are both the client ID.\n3. Keep the lifetime short, 5 to 10 minutes after `nbf`. If the window has passed you get [700024](https://brambrinkmeier.com/entra/aadsts/700024/) rather than 50027.\n4. Sign with the private key of a certificate registered on the app registration, using PSS padding.\n5. Let a library do it. MSAL builds the assertion from a certificate for you, and most 50027 in this case comes from hand-rolled code.\n6. Send it with `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, or you get [90023](https://brambrinkmeier.com/entra/aadsts/90023/) first.\n\n**For admins of a federated identity provider:**\n1. Work out which of Microsoft's eight conditions applies before changing anything. The `nonce`, `sub`, duplicate-claim and signature-verification bullets are all about an external ID token.\n2. Check the issuer and audience values configured on both sides, and that the provider's current signing key is the one Entra is reading. A key rollover on the provider gives *\"External ID token from issuer failed signature verification\"*.\n3. Check clock skew. *\"Not within its valid time range\"* is a time problem on the provider, not in Entra.\n\n**For admins generally:**\n1. Use the queries above to identify the app and its owner, and whether any source that failed also succeeded.\n2. If an address with no history for the app is sending assertions, treat the app ID as known to someone outside and review the app's permissions."
                  },
                  "labReproduced": {
                    "date": "2026-10-30",
                    "method": "Client-credentials token request against a lab tenant",
                    "scenario": "Client assertion that is not a JWT",
                    "message": "AADSTS50027: JWT token is invalid or malformed.",
                    "logCaptured": null,
                    "kqlTested": null
                  },
                  "labSectionText": "On 30 October 2026 an automated lab run sent a client-credentials token request with the correct `client_assertion_type` and a `client_assertion` that was not a JWT. Entra returned:\n\n> AADSTS50027: JWT token is invalid or malformed.\n\nBoth rounds of the run returned the same code and the same one-line message, short enough that the lab's 160-character cut does not truncate it. The run had expected 9002313, 50027 or 700027, and 50027 is the one it got. In the same run, the same request with an unaccepted `client_assertion_type` returned [90023](https://brambrinkmeier.com/entra/aadsts/90023/), and a syntactically valid assertion whose validity window had passed returned [700024](https://brambrinkmeier.com/entra/aadsts/700024/) with the current time and the assertion's `nbf` in the message. So the lab confirms the format case specifically, and not the other seven conditions in Microsoft's list.\n\nNo sign-in log row was captured. See [how pages are checked](https://brambrinkmeier.com/methodology/).",
                  "labRuns": [
                    {
                      "date": "2026-10-30",
                      "run": "round 4",
                      "method": "token-endpoint requests against a lab tenant",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/67",
                      "scenario": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "date": "2026-10-30",
                      "run": "round 5",
                      "method": "token-endpoint requests against a lab tenant, using lab-created test apps",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/69",
                      "scenario": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "date": "2026-10-30",
                      "run": "round 5 (first attempt)",
                      "method": "token-endpoint requests against a lab tenant",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/68",
                      "scenario": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "date": "2026-10-30",
                      "run": "round 6",
                      "method": "token-endpoint requests against a lab tenant, using lab-created test apps",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/70",
                      "scenario": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "date": "2026-10-30",
                      "run": "round 7",
                      "method": "token-endpoint requests against a lab tenant",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/71",
                      "scenario": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "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": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    },
                    {
                      "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": "Client assertion that is not a JWT",
                      "expected": "9002313 / 50027 / 700027",
                      "got": "50027",
                      "matchedExpectation": true,
                      "message": "AADSTS50027: JWT token is invalid or malformed."
                    }
                  ],
                  "verification": "lab-reproduced",
                  "related": [
                    "90023",
                    "700024",
                    "7000216",
                    "7000218"
                  ],
                  "coveredBy": [],
                  "sources": [
                    {
                      "title": "Microsoft Entra authentication and authorization error codes",
                      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
                    },
                    {
                      "title": "Microsoft identity platform application authentication certificate credentials",
                      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials"
                    },
                    {
                      "title": "Microsoft identity platform and the OAuth 2.0 client credentials flow",
                      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow"
                    },
                    {
                      "title": "AADServicePrincipalSignInLogs table reference (Azure Monitor)",
                      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs"
                    },
                    {
                      "title": "SigninLogs table reference (Azure Monitor)",
                      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs"
                    }
                  ],
                  "published": "2026-10-30",
                  "lastUpdated": "2026-10-30",
                  "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, \"AADSTS50027: JWT token is invalid or malformed (fix)\", https://brambrinkmeier.com/entra/aadsts/50027/ (CC BY 4.0)",
                  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=50027"
                }
              }
            }
          },
          "404": {
            "description": "No page for that code (the file does not exist)"
          }
        }
      }
    },
    "/entra/aadsts/{code}.md": {
      "get": {
        "operationId": "getAadstsCodeMarkdown",
        "summary": "Markdown twin of one AADSTS code page",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4,7}$"
            },
            "example": "50027"
          }
        ],
        "responses": {
          "200": {
            "description": "Markdown",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No page for that code"
          }
        }
      }
    },
    "/lab-runs.json": {
      "get": {
        "operationId": "listLabRuns",
        "summary": "Every automated lab run, redacted",
        "description": "Append-only: a run is never edited after the fact; a later run is a new record with its own date. A result is a measurement on that date and can differ from a later run.",
        "responses": {
          "200": {
            "description": "Lab runs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabRuns"
                },
                "example": {
                  "schemaVersion": "1.0",
                  "generatedAt": "2026-11-08T15:15:25.046Z",
                  "name": "Entra ID and Microsoft Graph error-code lab runs",
                  "description": "Every automated lab run recorded in data/lab-runs/ of the site repository: the scenario sent, the code expected, the code returned and the first line of the message as returned. Identifiers from the lab tenant (tenant id, app and object ids, addresses) are replaced with placeholders such as {tenant-id}.",
                  "semantics": "Append-only: a run is never edited after the fact; a later run is a new record with its own date. A result is a measurement on that date and can differ from a later run.",
                  "license": "CC BY 4.0",
                  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
                  "attribution": "Bram Brinkmeier, https://brambrinkmeier.com/ (Bram Brinkmeier), CC BY 4.0",
                  "method": "https://brambrinkmeier.com/methodology/",
                  "documentation": "https://brambrinkmeier.com/for-agents/",
                  "runCount": 14,
                  "resultCount": 505,
                  "runs": [
                    {
                      "id": "2026-10-25",
                      "kind": "token-endpoint",
                      "date": "2026-10-25",
                      "run": null,
                      "method": "ROPC token request against a lab tenant",
                      "workflow": ".github/workflows/lab-error-codes.yml",
                      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/38",
                      "note": "message is the first line of error_description as recorded in the issue, which cuts it at about 160 characters.",
                      "results": [
                        {
                          "scenario": "User does not exist",
                          "request": null,
                          "httpStatus": null,
                          "expected": "50034",
                          "got": "50034",
                          "matchedExpectation": true,
                          "message": "AADSTS50034: The user account {EUII Hidden} does not exist in the {tenant-id} directory. To sign into this application, the account mus"
                        },
                        {
                          "scenario": "Wrong password",
                          "request": null,
                          "httpStatus": null,
                          "expected": "50126",
                          "got": "50126",
                          "matchedExpectation": true,
                          "message": "AADSTS50126: Error validating credentials due to invalid username or password."
                        }
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/aadsts.json": {
      "get": {
        "operationId": "getAadstsDataset",
        "summary": "The site's own classification fields for every code (the Zenodo-archived dataset shape)",
        "responses": {
          "200": {
            "description": "Dataset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/graph-errors.json": {
      "get": {
        "operationId": "getGraphErrors",
        "summary": "Microsoft Graph error codes as returned by the live service in a lab tenant",
        "responses": {
          "200": {
            "description": "Graph error codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "operationId": "getLlmsFull",
        "summary": "All code page Markdown twins in one text file",
        "responses": {
          "200": {
            "description": "Plain text",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlms",
        "summary": "Site overview with links to every page and data file",
        "responses": {
          "200": {
            "description": "Plain text",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/checksums.json": {
      "get": {
        "operationId": "getChecksums",
        "summary": "SHA-256 of every machine-readable file in this build",
        "description": "Written after the build, so it cannot be an example here; shape: {\"algorithm\":\"sha256\",\"generatedAt\":\"<ISO 8601>\",\"files\":{\"/api/aadsts/index.json\":\"<hex>\", ...}}.",
        "responses": {
          "200": {
            "description": "Checksums",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Checksums"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "summary": "This document",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CodeIndex": {
        "type": "object",
        "required": [
          "schemaVersion",
          "generatedAt",
          "count",
          "labReproducedCount",
          "codes"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "license": {
            "type": "string"
          },
          "licenseUrl": {
            "type": "string",
            "format": "uri"
          },
          "attribution": {
            "type": "string"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "count": {
            "type": "integer"
          },
          "labReproducedCount": {
            "type": "integer",
            "description": "Pages whose frontmatter records a lab reproduction"
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "code",
                "title",
                "url",
                "json",
                "markdown",
                "labReproduced",
                "inMicrosoftReference"
              ],
              "properties": {
                "code": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "json": {
                  "type": "string",
                  "format": "uri"
                },
                "markdown": {
                  "type": "string",
                  "format": "uri"
                },
                "labReproduced": {
                  "type": "boolean"
                },
                "inMicrosoftReference": {
                  "type": "boolean",
                  "description": "false when Microsoft's error code reference does not list the code"
                },
                "attackSignal": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "high",
                    "medium",
                    "low",
                    null
                  ]
                },
                "lastUpdated": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date"
                }
              }
            }
          }
        }
      },
      "CodeRecord": {
        "type": "object",
        "required": [
          "schemaVersion",
          "code",
          "title",
          "url",
          "securityReading",
          "kql",
          "fix",
          "labReproduced",
          "labRuns",
          "verification",
          "sources",
          "license",
          "attribution"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Microsoft's error name, e.g. InvalidUserNameOrPassword"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTML page"
          },
          "alternates": {
            "type": "object",
            "properties": {
              "markdown": {
                "type": "string"
              },
              "json": {
                "type": "string"
              },
              "html": {
                "type": "string"
              }
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "description": "The short answer shown at the top of the page"
          },
          "inMicrosoftReference": {
            "type": "boolean"
          },
          "unlistedChecked": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "meaningText": {
            "type": [
              "string",
              "null"
            ],
            "description": "Markdown of the \"What does it mean?\" section"
          },
          "sections": {
            "type": "array",
            "description": "Every section of the page in page order",
            "items": {
              "type": "object",
              "properties": {
                "heading": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                }
              }
            }
          },
          "microsoftWording": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "text": {
                "type": "string"
              },
              "source": {
                "type": "string"
              },
              "sourceUrl": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "description": "Microsoft's wording as the page quotes it; null when the page quotes nothing"
          },
          "securityReading": {
            "type": "object",
            "properties": {
              "category": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "kind": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "attackSignal": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "level": {
                    "type": "string",
                    "enum": [
                      "high",
                      "medium",
                      "low"
                    ]
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "mitre": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              },
              "tables": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Sign-in log tables where the code appears"
              },
              "isItAnAttack": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Markdown of the \"Is it an attack?\" section"
              },
              "conditionalAccess": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "heading": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  }
                },
                "description": "The Conditional Access section when the page has one"
              }
            }
          },
          "kql": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "leadIn": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "query": {
                  "type": "string"
                }
              }
            },
            "description": "Every KQL block on the page"
          },
          "kqlSectionText": {
            "type": [
              "string",
              "null"
            ]
          },
          "fix": {
            "type": "object",
            "properties": {
              "short": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "steps": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "labReproduced": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "date": {
                "type": "string",
                "format": "date"
              },
              "method": {
                "type": "string"
              },
              "scenario": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "logCaptured": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              },
              "kqlTested": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              }
            },
            "description": "Present only when an automated lab run returned this code; null otherwise"
          },
          "labSectionText": {
            "type": [
              "string",
              "null"
            ]
          },
          "labRuns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabRow"
            },
            "description": "Rows from /lab-runs.json whose returned or expected code names this code"
          },
          "verification": {
            "type": "string",
            "enum": [
              "documentation",
              "lab-reproduced",
              "lab-verified"
            ]
          },
          "related": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "coveredBy": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "published": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "lastUpdated": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "lastVerified": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "license": {
            "type": "string"
          },
          "licenseUrl": {
            "type": "string"
          },
          "licenseNote": {
            "type": "string"
          },
          "attribution": {
            "type": "string"
          },
          "microsoftErrorLookup": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "LabRow": {
        "type": "object",
        "properties": {
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "run": {
            "type": [
              "string",
              "null"
            ]
          },
          "method": {
            "type": "string"
          },
          "issue": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reference to the GitHub issue that held the raw run output. Those issues are in a private repository; public copies of the runs are under lab/runs in the aadsts-dataset repository"
          },
          "scenario": {
            "type": "string"
          },
          "request": {
            "type": [
              "string",
              "null"
            ],
            "description": "Graph runs only"
          },
          "httpStatus": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Graph runs only"
          },
          "expected": {
            "type": [
              "string",
              "null"
            ],
            "description": "Code(s) the scenario was written to produce, \"/\"-separated when several were acceptable; null when the run file does not record it"
          },
          "got": {
            "type": [
              "string",
              "null"
            ],
            "description": "Code returned; null when no response came back at all"
          },
          "matchedExpectation": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "message": {
            "type": "string",
            "description": "First line of the message as returned, identifiers replaced with placeholders"
          }
        }
      },
      "LabRuns": {
        "type": "object",
        "required": [
          "schemaVersion",
          "generatedAt",
          "runCount",
          "resultCount",
          "runs"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "semantics": {
            "type": "string"
          },
          "license": {
            "type": "string"
          },
          "licenseUrl": {
            "type": "string"
          },
          "attribution": {
            "type": "string"
          },
          "method": {
            "type": "string"
          },
          "documentation": {
            "type": "string"
          },
          "runCount": {
            "type": "integer"
          },
          "resultCount": {
            "type": "integer"
          },
          "runs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "File name in data/lab-runs/ without extension"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "token-endpoint",
                    "graph"
                  ]
                },
                "date": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date"
                },
                "run": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "method": {
                  "type": "string"
                },
                "workflow": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "issue": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uri"
                },
                "note": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "results": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LabRow"
                  }
                }
              }
            }
          }
        }
      },
      "Checksums": {
        "type": "object",
        "required": [
          "algorithm",
          "generatedAt",
          "files"
        ],
        "properties": {
          "algorithm": {
            "type": "string",
            "const": "sha256"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "files": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        }
      }
    }
  }
}
