micronaut-projects / micronaut-security

The official Micronaut security solution
Apache License 2.0
170 stars 126 forks source link

OpenID error for Azure ActiveDirectory: "JWT signature validation failed for provider X" #90

Closed mbjarland closed 4 years ago

mbjarland commented 4 years ago

Edit: Just noticed that this issue seems to be duplicating issue #89, leaving it here anyway as there is a fair bit of information in this issue

OpenID error for Azure AD: JWT signature validation failed for provider'

When trying to use openid connect against azure active directory, the token signature validation always fails with the error:

14:48:12.158 INFO  [main] i.m.r.Micronaut - Startup completed in 5187ms. Server Running: http://localhost:8080
14:48:17.498 ERROR [nioEventLoopGroup-1-20] i.m.s.o.e.t.r.v.DefaultOpenIdTokenResponseValidator - JWT signature validation failed for provider [azuread]

my application.yaml configuration is as follows:

micronaut:
  security:
    enabled: true
    oauth2:
      enabled: true
      clients:
        azuread:
          client-id: xxxxxxxxxxxxxx
          client-secret: xxxxxxxxxxxxxxxx
          openid:
            issuer: https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f
    token:
      jwt:
        enabled: true
        cookie:
          enabled: true
        signatures:
          secret:
            generator:
              secret: pleaseChangeThisSecretForANewOne
              jws-algorithm: RS256

For high level context, this is how far we get in the process:

  1. micronaut successfully connecrts to the openid config which in my case is at:

    https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/.well-known/openid-configuration

  2. micronaut successfully sends the user to the azure authorization server
  3. the user successfully logs in
  4. azure successfully sends the user back to the micronaut application with the relevant access, refresh, id tokens attached.
  5. micronaut tries to validate the signature of the received token(s) using the json web keys (jwks) published in the relevant jwks_uri as published by the azure openid configuration json document.
  6. because the azure jwk set does not use the optional alg key and micronaut incorrectly assumes that this key is always there, the signature validation will always fail.

I.e. using micronaut openid against azure active directory will never work with the provided micronaut implementation.

Analysis and Comparison of OpenID providers

Microsoft Azure

To understand the issues, we need to look at the openid configurations and the jwk keys of azure as compared to the specification and some other relevant openid providers.

The azure openid config (which in my case is at https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/.well-known/openid-configuration) looks as follows:

{
  "token_endpoint": "https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/oauth2/token",
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "private_key_jwt",
    "client_secret_basic"
  ],
  "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys",
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "subject_types_supported": [
    "pairwise"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "response_types_supported": [
    "code",
    "id_token",
    "code id_token",
    "token id_token",
    "token"
  ],
  "scopes_supported": [
    "openid"
  ],
  "issuer": "https://sts.windows.net/8056f686-b27e-4792-950e-b2770b2a9a2f/",
  "microsoft_multi_refresh_token": true,
  "authorization_endpoint": "https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/oauth2/authorize",
  "http_logout_supported": true,
  "frontchannel_logout_supported": true,
  "end_session_endpoint": "https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/oauth2/logout",
  "claims_supported": [
    "sub",
    "iss",
    "cloud_instance_name",
    "cloud_instance_host_name",
    "cloud_graph_host_name",
    "msgraph_host",
    "aud",
    "exp",
    "iat",
    "auth_time",
    "acr",
    "amr",
    "nonce",
    "email",
    "given_name",
    "family_name",
    "nickname"
  ],
  "check_session_iframe": "https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/oauth2/checksession",
  "userinfo_endpoint": "https://login.microsoftonline.com/8056f686-b27e-4792-950e-b2770b2a9a2f/openid/userinfo",
  "tenant_region_scope": "EU",
  "cloud_instance_name": "microsoftonline.com",
  "cloud_graph_host_name": "graph.windows.net",
  "msgraph_host": "graph.microsoft.com",
  "rbac_url": "https://pas.windows.net"
}

this in turn gives us the jwks_uri https://login.microsoftonline.com/common/discovery/keys which contains the microsoft signing keys:

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "aPctw_odvROoENg3VoOlIh2tiEs",
      "x5t": "aPctw_odvROoENg3VoOlIh2tiEs",
      "n": "p2DzxOZiWEHhtVavuwImryTRxW4kJ0mbA1lbXon550DUnKDZCNZaztno8HpOl6NSbVbW-QLDz5VOqCn-PDvSIRcw-2hrJPRnCNob4yGEuC7v9dPVpPDFRiUrOcwCbJak6xsK9PEsX8FQ_onFHO6YJkjsFG8S2nMhgRK-JdURUcuj9paywSBtW9ddeqjQPgCPbZJtk39ReouoBYNm9xiwhTN0InY9Rt9PKUh4cRetg3OeKQ2E8TOVh1nHeTT2HIIYnAgB7ESUA07wYBuvet4UGemC2SdfpTSWk2YqzjZONW8p01hJg9x8lcSeyaQVOxTP_SjQoP99la1V8lArF35qxQ",
      "e": "AQAB",
      "x5c": [
        "MIIDBTCCAe2gAwIBAgIQU10WcpDECatD1ywgv0TNJjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE5MDgyNTAwMDAwMFoXDTI0MDgyNDAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdg88TmYlhB4bVWr7sCJq8k0cVuJCdJmwNZW16J+edA1Jyg2QjWWs7Z6PB6TpejUm1W1vkCw8+VTqgp/jw70iEXMPtoayT0ZwjaG+MhhLgu7/XT1aTwxUYlKznMAmyWpOsbCvTxLF/BUP6JxRzumCZI7BRvEtpzIYESviXVEVHLo/aWssEgbVvXXXqo0D4Aj22SbZN/UXqLqAWDZvcYsIUzdCJ2PUbfTylIeHEXrYNznikNhPEzlYdZx3k09hyCGJwIAexElANO8GAbr3reFBnpgtknX6U0lpNmKs42TjVvKdNYSYPcfJXEnsmkFTsUz/0o0KD/fZWtVfJQKxd+asUCAwEAAaMhMB8wHQYDVR0OBBYEFPBE/OYhU7DwWnEa6luL8L+MZwbHMA0GCSqGSIb3DQEBCwUAA4IBAQAYyA81g/dfsm/AeUyDfzObRaEdKinKI5GUFUvJXDobED7f6NL+ECyULBEVm/ksZBrg6f0aPTDnSFVsZIfMogXc0KfJrII1lnXucbt1LCOmjdlf54J1R/mn9dkHyZ3pfoZtpqcXlKFnRCurn864XqRQFgBSG39xUjXXUR5vWSrp3mHlil+W9Z9RTImNmkXnSJDosYLEvCUYyqarV8rKj6rBfaBdqP3F5s4GwIdjsZ13YfkD4c+meX3W/9x74awB5ys+p78c7IjnO8mQB9kPvY9wEnGLDfLQEC+A0af81ybvevMraFfwZtsq/FYJEMnn6hKkTUeb1kPpVdJLVN4JqiUM"
      ]
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "BB8CeFVqyaGrGNuehJIiL4dfjzw",
      "x5t": "BB8CeFVqyaGrGNuehJIiL4dfjzw",
      "n": "nYf1jpn7cFdQK2VuZevofmjBjLXldOXe92k5ktSSTg5X0sywHWmGM2n7CCXbx4CCs01-7gFNWUd1H3Ho1OtKIhqmxiPPMTPiY6ZGHUHDm0nGK3RUQafTT9kQ2eJOOB4QViAMdjCOt9lDp0REEWLDU5BvYgbl_cou3H3aVRd4hntm9No-RSlzhB3rBBmZaDM-pYWhxGwkBMbJnNeKJdBStz1xWqbVvCzc_SUUFyo22_4AoNgpPkhFguzIKS55AL1HotQKxlUPttUiR5C4DeJ6EkogQCWT97ePkThVoJGzrjZqNv_P2QHJOXbEvaTQB5kZzz9FzLtJCfQsFwk1kan9Iw",
      "e": "AQAB",
      "x5c": [
        "MIIDBTCCAe2gAwIBAgIQbiJkXaenk61AKixVocnLRTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE5MTAwNTAwMDAwMFoXDTI0MTAwNDAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ2H9Y6Z+3BXUCtlbmXr6H5owYy15XTl3vdpOZLUkk4OV9LMsB1phjNp+wgl28eAgrNNfu4BTVlHdR9x6NTrSiIapsYjzzEz4mOmRh1Bw5tJxit0VEGn00/ZENniTjgeEFYgDHYwjrfZQ6dERBFiw1OQb2IG5f3KLtx92lUXeIZ7ZvTaPkUpc4Qd6wQZmWgzPqWFocRsJATGyZzXiiXQUrc9cVqm1bws3P0lFBcqNtv+AKDYKT5IRYLsyCkueQC9R6LUCsZVD7bVIkeQuA3iehJKIEAlk/e3j5E4VaCRs642ajb/z9kByTl2xL2k0AeZGc8/Rcy7SQn0LBcJNZGp/SMCAwEAAaMhMB8wHQYDVR0OBBYEFOLhl3BDPLNVYDe38Dp9JbUmd4kKMA0GCSqGSIb3DQEBCwUAA4IBAQAN4XwyqYfVdMl0xEbBMa/OzSfIbuI4pQWWpl3isKRAyhXezAX1t/0532LsIcYkwubLifnjHHqo4x1jnVqkvkFjcPZ12kjs/q5d1L0LxlQST/Uqwm/9/AeTzRZXtUKNBWBOWy9gmw9DEH593sNYytGAEerbWhCR3agUxsnQSYTTwg4K9cSqLWzHX5Kcz0NLCGwLx015/Jc7HwPJnp7q5Bo0O0VfhomDiEctIFfzqE5x9T9ZTUSWUDn3J7DYzs2L1pDrOQaNs/YEkXsKDP1j4tOFyxic6OvjQ10Yugjo5jg1uWoxeU8pI0BxY6sj2GZt3Ynzev2bZqmj68y0I9Z+NTZo"
      ]
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "M6pX7RHoraLsprfJeRCjSxuURhc",
      "x5t": "M6pX7RHoraLsprfJeRCjSxuURhc",
      "n": "xHScZMPo8FifoDcrgncWQ7mGJtiKhrsho0-uFPXg-OdnRKYudTD7-Bq1MDjcqWRf3IfDVjFJixQS61M7wm9wALDj--lLuJJ9jDUAWTA3xWvQLbiBM-gqU0sj4mc2lWm6nPfqlyYeWtQcSC0sYkLlayNgX4noKDaXivhVOp7bwGXq77MRzeL4-9qrRYKjuzHfZL7kNBCsqO185P0NI2Jtmw-EsqYsrCaHsfNRGRrTvUHUq3hWa859kK_5uNd7TeY2ZEwKVD8ezCmSfR59ZzyxTtuPpkCSHS9OtUvS3mqTYit73qcvprjl3R8hpjXLb8oftfpWr3hFRdpxrwuoQEO4QQ",
      "e": "AQAB",
      "x5c": [
        "MIIC8TCCAdmgAwIBAgIQfEWlTVc1uINEc9RBi6qHMjANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTgxMDE0MDAwMDAwWhcNMjAxMDE0MDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEdJxkw+jwWJ+gNyuCdxZDuYYm2IqGuyGjT64U9eD452dEpi51MPv4GrUwONypZF/ch8NWMUmLFBLrUzvCb3AAsOP76Uu4kn2MNQBZMDfFa9AtuIEz6CpTSyPiZzaVabqc9+qXJh5a1BxILSxiQuVrI2BfiegoNpeK+FU6ntvAZervsxHN4vj72qtFgqO7Md9kvuQ0EKyo7Xzk/Q0jYm2bD4SypiysJoex81EZGtO9QdSreFZrzn2Qr/m413tN5jZkTApUPx7MKZJ9Hn1nPLFO24+mQJIdL061S9LeapNiK3vepy+muOXdHyGmNctvyh+1+laveEVF2nGvC6hAQ7hBAgMBAAGjITAfMB0GA1UdDgQWBBQ5TKadw06O0cvXrQbXW0Nb3M3h/DANBgkqhkiG9w0BAQsFAAOCAQEAI48JaFtwOFcYS/3pfS5+7cINrafXAKTL+/+he4q+RMx4TCu/L1dl9zS5W1BeJNO2GUznfI+b5KndrxdlB6qJIDf6TRHh6EqfA18oJP5NOiKhU4pgkF2UMUw4kjxaZ5fQrSoD9omjfHAFNjradnHA7GOAoF4iotvXDWDBWx9K4XNZHWvD11Td66zTg5IaEQDIZ+f8WS6nn/98nAVMDtR9zW7Te5h9kGJGfe6WiHVaGRPpBvqC4iypGHjbRwANwofZvmp5wP08hY1CsnKY5tfP+E2k/iAQgKKa6QoxXToYvP7rsSkglak8N5g/+FJGnq4wP6cOzgZpjdPMwaVt5432GA=="
      ]
    }
  ]
}

For reference I will paste in a couple of relevant alternative openid provider configuartions before continuing with the analysis of why azure does not work.

Google Cloud

The same process using google; openid config:

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "revocation_endpoint": "https://oauth2.googleapis.com/revoke",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "response_types_supported": [
    "code",
    "token",
    "id_token",
    "code token",
    "code id_token",
    "token id_token",
    "code token id_token",
    "none"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "profile"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "client_secret_basic"
  ],
  "claims_supported": [
    "aud",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "iss",
    "locale",
    "name",
    "picture",
    "sub"
  ],
  "code_challenge_methods_supported": [
    "plain",
    "S256"
  ]
}

which gives us the jwks_uri https://www.googleapis.com/oauth2/v3/certs with the following data::

{
  "keys": [
    {
      "use": "sig",
      "kid": "8a63fe71e53067524cbbc6a3a58463b3864c0787",
      "e": "AQAB",
      "kty": "RSA",
      "alg": "RS256",
      "n": "ybPLfOiobkYqPJf2S98TkP0YdvzySqPSANa1Rw8QEtT2Ixq2fu1DFOn98re1qjYn5HQPlW6Hvn__w9JmlGd-mKg_XJvvEa81Tm-tP9tTrgflAizjbTHfX9-n9NFlKa_1blJ4HKYn1vkhmwhCPmuyXeb7sHZBInvvjl4laVB3VVwigqgreZBF65aodMcVkIqzB-6Vy_WKeRqAR-sNcJf8sdSNvOSwPPQ_gw3MQ38SiemxVVkfnn6AWichuEI97S264IuzgNJF5TippTPjzjJv-qxisRDqMmvaeki1PCe8CY9EKQJxBWZOy6Wlbdy1gmzOeLsXEVL5IsFyLGX6Tq12mQ"
    },
    {
      "kid": "9cef5340642b157fa8a4f0d874fe7900362d82db",
      "e": "AQAB",
      "kty": "RSA",
      "alg": "RS256",
      "n": "tUXNIN6LZJX5ra23GAWzPQ2zJfjwQxztau6bKDQH_ehhJ5CCBDpBcIyHebG5WCOIN_N_vqZUoeYvqXKVfpmUIW4O_rFnKgP7K-Mal4VBqOtmDs0z9HKz712wU6GmWqQnJBIDzToTgK5EORSMZHtZvZr6jvryZYzZly8Bit2bMauQt3OYlGlYArDK2Gy6E6orqIzY2O_mRQE0uENwuxtZHBIo8joOwEFfFjN6kURNjT0KqFeO28z-0FosiiyTrq2NrjhXdiRxus0t1fq_xJ14AHNaPzLjzYb6UJ0EJE5x_wuUvBDMbjvS1Zlr8EV8pCBzeqMnHxvvw9lkWCK0zKOukw",
      "use": "sig"
    }
  ]
}

Amazon AWS

I didn't have the time nor inclination to setup an openid config with aws, but the jwks_uri for aws is https://cognito-identity.amazonaws.com/.well-known/jwks_uri and the relevant keys data looks as follows:

{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ap-northeast-11",
      "n": "AI7mc1assO5n6yB4b7jPCFgVLYPSnwt4qp2BhJVAmlXRntRZ5w4910oKNZDOr4fe/BWOI2Z7upUTE/ICXdqirEkjiPbBN/duVy5YcHsQ5+GrxQ/UbytNVN/NsFhdG8W31lsE4dnrGds5cSshLaohyU/aChgaIMbmtU0NSWQ+jwrW8q1PTvnThVQbpte59a0dAwLeOCfrx6kVvs0Y7fX7NXBbFxe8yL+JR3SMJvxBFuYC+/om5EIRIlRexjWpNu7gJnaFFwbxCBNwFHahcg5gdtSkCHJy8Gj78rsgrkEbgoHk29pk8jUzo/O/GuSDGw8qXb6w0R1+UsXPYACOXM8C8+E=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ap-northeast-21",
      "n": "AIHzdsSdHLX/a2jBt9GjpX1cvnWmeKhKgA3Pa2d5lTWMuQlWqP8yRnMDvH4j8yrzkf3uTSUVtfHYUDwYvXTjQMKyw1DYprrCo6g0aKThVmCvgfGCL2nWSiAcql6qnAUMhvvyTLZkPCLGgJnqUxuwkxYzs+hoXrNx2WKUyNOVIGXEVCxJBXJaWe4ERrk9iiu022UPmZuQwsHvf010eH7tHhw03MZJ9lTVpC+A+rSgGuWUo8Nb3d8OcBf3ObjL4gQ9EeclhXSt7TUnXR0NxHnErGiDAE6FGePRbAFoAJWoSsM4FoqixGr6E0uTsA5IfJ+VnveYCTpqsuxYy/lupZNxwRE=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ap-south-11",
      "n": "AIgVHiyTeMG5uNWFBR5lombNayt53U0nWpBFWTUsv2Wey9O2hsUVnAZGlcziy6g9X7E/EDS+itQ084tIeFs7hfvDgJd/GiT4nxt34wTxu59c4Lw8XmrDQD6YaPu0BocWoWN/ukh6yBjJSG3iUv2fVNaG+HeYr0dRmw1hmUhX3RQkjmgJMh6UZetjsw11VgxOeVqS9vTbyVZQhIFMEYZh9upyLFVSwsb6PaSwv0I5+RNaIUjiFmSC1dwzkoRcldluXMOuf+Rb+7/y9tMqHFy2WBvruhuUUDmT7+BFuhujl/IyUmelbNrWLdSGXEsCJf11OvBtocv3zCnS04DefXNJY8M=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ap-southeast-11",
      "n": "AIjiMJhloeH+hIzx/hpgny9zFWy+dpu03F3fXoijkq7iohjbzxqmF1iAsrx12v7I1VENN72VoALEHBIvF807fmclwF5+7R3EjzMyP/SJgEWYKecCBlk23QKGBTmDvm22/X76IIUEdlMHC/Rm88iDGchQV0Hw6jtTOJMyIuhqL/foGrJLOuwq2Jhbg6o3GZDWY8JRkRCCKV+yJZLpDLtnBG8fM2Z0bDmGJHbnHyyFDmoqAWbeSZKS2K+LnHoqd+wbQaYpTnX2fVq2AEiz+I6RD+tvWegG5Lgw5xnuST9R9d3Xp++mIg8YsSoD8agtVwOh6qxkYnq4vEEGaU8Qsm+BeNs=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ap-southeast-22",
      "n": "AJZzNUBnF1H6rFFiqJbiziWW7VVbyoXWH7CTUMOYzJo/7WsyJkPt95z7iLvTPR26TWg2oQIKd5Di/B5qRuPq3sg0LyEwM9QCRNyJ3be9rLSOkLCFAtwwxgonpJkMSpFwmrlrxcXQMF0xyz9IXPRgrI9KlCG0Xd/BQnV79zeMDObwMZXzj8ki1Xuh06R5XGvacNds72H5oByjeoNYzhMktqVO8pWlNKbRATyPi/HwdG8DhNH5G4TPXiBMwNhp3W/lK4JhMMgbJ01y5Xq/32ib1qSxp8ec1LKkoJdbiWxWkpXLPUahEN38+J1NeVAH+Nv8ZMqoV9n2IGIts0UJY8a0ZWc=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "ca-central-11",
      "n": "AOj6hiK7+Xc95sDrRxq6+DIj7votGhbsLZUuOAd4leTB390jTcL1JfK77WzPi8MtaeTiGNyykdQ0HJ/qHfBoAPUPh/yhn0vXC3d7vkAn6YM0vtc9hfdMXm47yaUeIR3QIu8qwLhHzTu1q2O1QqzYYT2dftg4X55f3TZNg88GE9HIj83V/xa+8bg4gRlFHYglb6jXANh08R8goBjxjMFWg7SS5V48L5GaqYef7/RszpXIMxyYriOq9fIF0nq43zmk7KFT7/fGlXIbWcimuJKQZfTJxcMp9JK/H6YzEUTJntlFeXgYnCvLCzmRKr0i3UsgD2xJaJoOqvUtRwE3W2kZ/i8=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "eu-central-11",
      "n": "AL9Kz62JHMpn5kBEqyoaXkM56x3l3Wi0kg0Juv71QtXo5M4ZJYxouKdcrKfevYTRNm6DE0hTbJnyj7Bh4EYbmruGdSWE970xkcFJxcgak0j4rneRX5G1E/xN27M42OOLmZCe8O6l3nksD0XGOqBPqOSEP3pYCNAYMncpSGnit56fUX+yszfMjGP3DVSUFZKtXbqwt/S0VpBi5BQbbD57R8DKenQsPfln91tgGopmXP66vZ4yWRUzs/mqHxcez3FcgHHXc6AbEJ6GOSVd9t+BCUW5kVY0aYO301PJczvB3zfsI6qebjS6BFTvMp8SqK532ZRnXEMgs/5gc9cfxpDsgvk=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "eu-west-11",
      "n": "AKriovi9cnm+07tkfZMFFCvjfobq0TP1qjrQQ4uS91P3mt/Wy3bdVjMt6DfZeuowwfdQdcsc0XgDV1KHlIG5PKj3v6q6uH2M0mcqFpQZnIQ0xUbRoZkR6bHFgdRHR2GTbm79nh3z1gsaVYeDFGLrE7gXNQRAoKtClif4cW+ZmLAfS2nPFAg61pryh/HUdaN2zvfbTGZaB1zVL41tX5DncoQx3COLpIcdKIB//CpWO0iVubU7ZnNPRVt079t5MUpwgtyAnhqMzYWElsTAPopEWVMTxHJr1LUKXiU6nX1UoX8OxUtBCZ30xLddGXw6e8G7dZKxq/es+ov8r7IlmTI7OXM=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "eu-west-21",
      "n": "AKPfdgeKVzCBNDbaywRWIx2/g9IIs9s4BntmaRhbhCswYjkdMLeNC6ZAydxOn8NYYdAEE29bpHtF7jpoSe6fXShOv5n/sRVlRVWEo/NuTOVckLcpqRpcjm1ujM/CA/1O16w9MyHz8tmzapG9VikHdpCh8URkaPnyuEcO2+cOP1jAZ1P2U9bhx9cKxXfe1Vr9DrvDexCVqQ0vLw0ZbjN7nU2yAkim7O1CX6+fOMTsEMC+WX+fDb0RZVJ2hPqUT+dDY2Nnta69/8rI51C5f5+NVjKr+DgHYeaPGmq2AZ762PWsCKcNU8UgB+guXM2UxoRU+V0DLVWgtf8AxojhWgpJntM=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "us-east-11",
      "n": "AIvLE/h4h9XdAVyy0C7fn1ZXZ3Gt6YT2LPsHsoCUGgPAVJnLJjPRj3dSI2UmlWaLacSoHYeFABfxj8YROnE9fpiGto5LcdyfuRKET9Nv5UaZp0kMSSoF7wXinp07ACUbn+ZE3ImQR16r1/Q/j3AD4CmN7gVjk5+EZzVCTQtAzJZJ8/EgCPFE4YA0Q2UgFtBjZnt4SI8TljikBqUmNDVKyjh2yI+m4fQO/LZOEaI/aGOWYen4RrO+/3hTYk73b+oFCPnIp1sLNUmdAHzjIgWYCC2qBwC+tRWi7065ea2KYj+kNNFevXFYMrph3U1mxqDZSzIOvEXIlZqlhOoOc5NmWMc=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "us-east-21",
      "n": "AIrl+VgezCW0WK81WBUNl6CIuuP+nI7TnKgKvNKMdpuHmUhdwDJwxLr+Qe4G6jQSpHNzuwRoQ8zPxkkyeHP5722tmtjUFBtx6GJ6YCIGAIGNuFRIr3uWWLUVF8IVbLswq/XbuvN8oJNl0039AlUOgTI0SdHAGenQjOHTA2Mx274JDrK85UNC1euAzcEEdYmBHWE/llRLmjy5JexKS/i+B23S+18FA+W5s2+kDCnaIn3iHbMUzsoRdn616C738KxiCHIGYP9JSwYy/2IqqSeRE/0qLAxZ1cYU3UkihTSkzsCNnvhqOf3Hn5mR4onfh4iWEghV8lCS6wywFoxAuPMG6Uc=",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "kid": "us-west-21",
      "n": "AJM4O/eTg8U00rbo/xXwECyAmpF8EUvbBj+nMvebhExjWyNhaEB27QvESbdM3FS+k8opxC5TKVqNCY8GGVAvHkTh5+BsaIeFrJLj22rXXs6E3bse5MBlmUHCIy8PQaZ/BpJWvlSz7IoprdfhgfOGvT96GgXouSytanvkU4A8a3jcmy2ZmdHSdeGLuAYQtdz+xP8zt2s2v2evFY/bEGQpd0EBlWsQKZvtZ0DJ1CtA/SJixbdhCj7bGh322QqgA8im+s3AAnD4I/UgfvZEAbkH6zqYgWTq5QnMsoizESf+6EmSDaJA4Bbkv1ffmcHuTEUwKnKZ+d7G5YXWwWndXu4tleM=",
      "e": "AQAB"
    }
  ]
}

Micronaut Code

Digging into the micronaut internals, we notice that the problem occurs in the following code:

    @Override
    public Optional<JWT> validate(OauthClientConfiguration clientConfiguration,
                                  OpenIdProviderMetadata openIdProviderMetadata,
                                  OpenIdTokenResponse openIdTokenResponse,
                                  @Nullable String nonce) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("Validating the JWT signature using the JWKS uri [{}]", openIdProviderMetadata.getJwksUri());
        }
        Optional<JWT> jwt = JwtTokenValidatorUtils.parseJwtIfValidSignature(openIdTokenResponse.getIdToken(),
                Collections.singletonList(new JwksSignature(openIdProviderMetadata.getJwksUri(), null, jwkValidator)),
                Collections.emptyList());

        if (jwt.isPresent()) {

        }

in class DefaultOpenIdTokenResponseValidator. The call to parseJwtIfValidSignature will eventually call the following code in JwrTokenValidationUtils:

    public static Optional<JWT> validateSignedJWTSignature(SignedJWT signedJWT,
                                                           List<SignatureConfiguration> signatureConfigurations) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("JWT is signed");
        }

        final JWSAlgorithm algorithm = signedJWT.getHeader().getAlgorithm();
        for (final SignatureConfiguration config : signatureConfigurations) {
            if (config.supports(algorithm)) {
            ...
           } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("{}", config.supportedAlgorithmsMessage());
                }
            }            

where the config.supports(algorithm) call will call the following code in class JwksSignature:

    public boolean supports(JWSAlgorithm algorithm) {
        return getJsonWebKeys()
                .stream()
                .map(JWK::getAlgorithm)
                .anyMatch(algorithm::equals);
    }

here we get to the crux of the problem. In the azure case, we have three json web keys so the the getJsonWebKeys call will return a list of three items. None of them however have a value for the algorithm (missing alg key in the keys data above) property which means that the algorithm::equals call will always return false (since it is essentially algorithm.equals(null) in each case).

I.e. the config.supports(...) call earlier will always return false, thus failing the jwt signature validation and resulting in the logged error.

Further, if debug logging is enabled, the else clause in JwrTokenValidationUtils will execute the following line:

                if (LOG.isDebugEnabled()) {
                    LOG.debug("{}", config.supportedAlgorithmsMessage());
                }

where the JwksSignature.supportedAlgorithmsMessage() looks as follows:

    public String supportedAlgorithmsMessage() {
        String message = getJsonWebKeys().stream()
                .map(JWK::getAlgorithm)
                .map(Algorithm::getName)
                .reduce((a, b) -> a + ", " + b)
                .map(s -> "Only the " + s)
                .orElse("No");
        return message + " algorithms are supported";
    }

which in the azure ad case will throw an NPE...again, because the getAlgorithm method of the JWK class will return null. So we have a logged error and if you try to get more information by turning on trace logging, you get an NPE instead.

Reproducing Using Standalone Groovy Script

The following groovy script downloads the relevant micronaut dependency and emulates the situation when connecting to the three above mentioned providers, azure, google, and aws:

@Grab('io.micronaut:micronaut-security-jwt:1.2.2')

import io.micronaut.security.token.jwt.signature.jwks.*

def jwksUris = [
  azure: 'https://login.microsoftonline.com/common/discovery/keys',
  google: 'https://www.googleapis.com/oauth2/v3/certs',
  aws: 'https://cognito-identity.amazonaws.com/.well-known/jwks_uri'
]

jwksUris.each { provider, jwksUri -> 
  def signature = new JwksSignature(jwksUri, null, null)
  def algMessage = getAlgMessage(signature)

  println "provider: $provider"
  println "  alg message: ${algMessage}"
  signature.jsonWebKeys.each { key -> 
    println "  kid ${key.keyID} -> alg ${key.algorithm}"
  }
}

def getAlgMessage(signature) {
  def algMessage = ''
  try { 
    algMessage = signature.supportedAlgorithmsMessage()
  } catch (NullPointerException e) {
    algMessage = "<NullPointerException>"
  }

  algMessage
}

executing this script results in the following output:

provider: azure
  alg message: <NullPointerException>
  kid aPctw_odvROoENg3VoOlIh2tiEs -> alg null
  kid BB8CeFVqyaGrGNuehJIiL4dfjzw -> alg null
  kid M6pX7RHoraLsprfJeRCjSxuURhc -> alg null
provider: google
  alg message: Only the RS256, RS256 algorithms are supported
  kid 8a63fe71e53067524cbbc6a3a58463b3864c0787 -> alg RS256
  kid 9cef5340642b157fa8a4f0d874fe7900362d82db -> alg RS256
provider: aws
  alg message: Only the RS512, RS512, RS512, RS512, RS512, RS512, RS512, RS512, RS512, RS512, RS512, RS512 algorithms are supported
  kid ap-northeast-11 -> alg RS512
  kid ap-northeast-21 -> alg RS512
  kid ap-south-11 -> alg RS512
  kid ap-southeast-11 -> alg RS512
  kid ap-southeast-22 -> alg RS512
  kid ca-central-11 -> alg RS512
  kid eu-central-11 -> alg RS512
  kid eu-west-11 -> alg RS512
  kid eu-west-21 -> alg RS512
  kid us-east-11 -> alg RS512
  kid us-east-21 -> alg RS512
  kid us-west-21 -> alg RS512

What the Specification Says

As defined by RFC7517 for "JSON Web Key (JWK)", the alg key is optional:


4.4.  "alg" (Algorithm) Parameter

   The "alg" (algorithm) parameter identifies the algorithm intended for
   use with the key.  The values used should either be registered in the
   IANA "JSON Web Signature and Encryption Algorithms" registry
   established by [JWA] or be a value that contains a Collision-
   Resistant Name.  The "alg" value is a case-sensitive ASCII string.
   Use of this member is OPTIONAL.

Note the ending:

Use of this member is OPTIONAL.

mbjarland commented 4 years ago

As noted in issue #89, the ID token returned by azure ad contains the following type of header:

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "aPctw_odvROoENg3VoOlIh2tiEs",
  "kid": "aPctw_odvROoENg3VoOlIh2tiEs"
}

where the kid specifically identifies the key that was used to to sign this token. We can find this same key id in the keys published by microsoft in the above analysis. I am no expert in jwt, jwk etc, but seems to me that the micronaut code should be using the indicated key directly instead of looping through and looking for one which supports the algorithm (which in this case does not exist).

graemerocher commented 4 years ago

@mbjarland since you have done all this analysis to identify the issue, fancy sending a PR to resolve the problem?

goeh commented 4 years ago

Seems like issue https://github.com/micronaut-projects/micronaut-security/issues/50 is also related.

mbjarland commented 4 years ago

@graemerocher I have spent some significant time mulling on this. A few big take aways:

  1. I have a lot to learn about jwt, jwk, jws, jwe and friends
  2. with that said, after reading and re-reading the specs etc it seems to me that just removing the supported algorithms check will resolve this problem. i.e. just remove the if(config.supports(algorithm)) clause will solve this as the contained if (config.verify(signedJWT)) call with match against the kid key id anyway which is exactly what the spec tells us to do. I will create a PR for this shortly.
  3. with all that said, the openid spec has the following to say about id token validation:

<<<< Quoting the openid spec

3.1.3.7. ID Token Validation

Clients MUST validate the ID Token in the Token Response in the following manner:

  1. If the ID Token is encrypted, decrypt it using the keys and algorithms that the Client specified during Registration that the OP was to use to encrypt the ID Token. If encryption was negotiated with the OP at Registration time and the ID Token is not encrypted, the RP SHOULD reject it.
  2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.
  3. The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
  4. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
  5. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
  6. If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer.
  7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client in the id_token_signed_response_alg parameter during Registration.
  8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512, the octets of the UTF-8 representation of the client_secret corresponding to the client_id contained in the aud (audience) Claim are used as the key to validate the signature. For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or if an azp value is present that is different than the aud value.
  9. The current time MUST be before the time represented by the exp Claim.
  10. The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific.
  11. If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.
  12. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate. The meaning and processing of acr Claim Values is out of scope for this specification.
  13. If the auth_time Claim was requested, either through a specific request for this Claim or by using the max_age parameter, the Client SHOULD check the auth_time Claim value and request re-authentication if it determines too much time has elapsed since the last End-User authentication.

end of openid spec quote >>>>

If I read the code correctly, and this is a tad complex so I might be missing something, micronaut is currently not doing a lot of the above. It might be worth having somebody take a somewhat longer look at the id token validation process to make sure we adhere to the spec.

alvarosanchez commented 4 years ago

@mbjarland thank you very much for your contribution. We truly appreciate your detailed analysis plus the accompanying PR. I have just merged it. Keep going!