Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix deserialize incompatibility with Okta ver claim #743

Closed

Conversation

kinsersh
Copy link

@kinsersh kinsersh commented May 18, 2022

Okta includes a "ver" claim in the ID tokens it issues that gets serialized to json, as follows:
{
"@Class": "java.util.Collections$UnmodifiableMap",
"ver": [
"java.lang.Long",
1
]
}

This claim is added by Okta automatically (see https://developer.okta.com/docs/reference/api/oidc/#id-token).

Spring Authorization Server encounters an error when attempting to deserialize this claim from Okta, as follows:
The class with java.lang.Long and name of java.lang.Long is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See spring-projects/spring-security#4370 for details
java.lang.IllegalArgumentException: The class with java.lang.Long and name of java.lang.Long is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See spring-projects/spring-security#4370 for details
at org.springframework.security.jackson2.SecurityJackson2Modules$AllowlistTypeIdResolver.typeFromId(SecurityJackson2Modules.java:253)

The issue at #567 has a similar symptom, though the situation being addressed in this pull request is different in that it would be encountered by any project wanting to do OpenID Connect with Okta. I thus recommend a general fix rather than a project-specific fix as recommended at #397.

This commit defines a mixin that doesn't alter the json formatting but does instruct SecurityJackson2Modules.AllowlistTypeIdResolver to permit the deseralization of that Okta claim.

Okta includes a "ver" claim in the ID tokens it issues that gets serialized to json, as follows:
{
  "@Class": "java.util.Collections$UnmodifiableMap",
  "ver": [
    "java.lang.Long",
    1
  ]
}

This claim is added by Okta automatically (see https://developer.okta.com/docs/reference/api/oidc/#id-token).

Spring Authorization Server encounters an error when attempting to deserialize this claim from Okta, as follows:
The class with java.lang.Long and name of java.lang.Long is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See spring-projects/spring-security#4370 for details
java.lang.IllegalArgumentException: The class with java.lang.Long and name of java.lang.Long is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See spring-projects/spring-security#4370 for details
	at org.springframework.security.jackson2.SecurityJackson2Modules$AllowlistTypeIdResolver.typeFromId(SecurityJackson2Modules.java:253)

The issue at spring-projects#567 has a similar symptom, though this situation would be encountered by any project wanting to do OpenID Connect with Okta. I thus recommend a general fix rather than a project-specific fix as recommended at spring-projects#397.

This commit defines a mixin that doesn't alter the json formatting but does instruct SecurityJackson2Modules.AllowlistTypeIdResolver to permit the deseralization of that Okta claim.

For broader context, below is an example of what is stored by Spring Authorization Server in the attributes column of the oauth2_authorization table.
{
  "@Class": "java.util.Collections$UnmodifiableMap",
  "org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest": {
    "@Class": "org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest",
    "authorizationUri": "http://localhost:5000/oauth2/authorize",
    "authorizationGrantType": {
      "value": "authorization_code"
    },
    "responseType": {
      "value": "code"
    },
    "clientId": "login-acceptance-test-client",
    "redirectUri": "http://127.0.0.1:5000/login-acceptance-test-client",
    "scopes": [
      "java.util.Collections$UnmodifiableSet",
      [
        "openid"
      ]
    ],
    "state": "foo",
    "additionalParameters": {
      "@Class": "java.util.Collections$UnmodifiableMap"
    },
    "authorizationRequestUri": "http://localhost:5000/oauth2/authorize?response_type=code&client_id=login-acceptance-test-client&scope=openid&state=foo&redirect_uri=http://127.0.0.1:5000/login-acceptance-test-client",
    "attributes": {
      "@Class": "java.util.Collections$UnmodifiableMap"
    }
  },
  "java.security.Principal": {
    "@Class": "org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken",
    "principal": {
      "@Class": "org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser",
      "authorities": [
        "java.util.Collections$UnmodifiableSet",
        [
          {
            "@Class": "org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority",
            "authority": "ROLE_USER",
            "idToken": {
              "@Class": "org.springframework.security.oauth2.core.oidc.OidcIdToken",
              "tokenValue": "actualTokenValueRemoved",
              "issuedAt": 1652411922.000000000,
              "expiresAt": 1652415522.000000000,
              "claims": {
                "@Class": "java.util.Collections$UnmodifiableMap",
                "at_hash": "X4TpvHXTMw3kaBMRas-H6A",
                "sub": "00u4zn2e0w8cRRFv75d1",
                "ver": [
                  "java.lang.Long",
                  1
                ],
                "amr": [
                  "java.util.ArrayList",
                  [
                    "pwd"
                  ]
                ],
                "iss": [
                  "java.net.URL",
                  "https://dev-231911394952.okta.com"
                ],
                "preferred_username": "login.at.okta.public.local",
                "nonce": "pWQDfOxvpslr_FWiTqHlIRdCiT5Sfq-PWvBmSyrDki0",
                "aud": [
                  "java.util.ArrayList",
                  [
                    "0oa4varwxxjIMcDph5d7"
                  ]
                ],
                "idp": "00otba6t5x5dSt78H5d6",
                "auth_time": [
                  "java.time.Instant",
                  1652411920.000000000
                ],
                "name": "login at okta public local test",
                "exp": [
                  "java.time.Instant",
                  1652415522.000000000
                ],
                "iat": [
                  "java.time.Instant",
                  1652411922.000000000
                ],
                "email": "login.at.okta.public.local@example.com",
                "jti": "ID.IfAKG8WvCxOy023DLl6I-WY-4pHgxMJZv1F79rqAtYM"
              }
            },
            "userInfo": {
              "@Class": "org.springframework.security.oauth2.core.oidc.OidcUserInfo",
              "claims": {
                "@Class": "java.util.Collections$UnmodifiableMap",
                "sub": "00u4zn2e0w8cRRFv75d1",
                "zoneinfo": "America/Los_Angeles",
                "email_verified": true,
                "updated_at": [
                  "java.time.Instant",
                  1652216788.000000000
                ],
                "name": "login at okta public local test",
                "preferred_username": "login.at.okta.public.local",
                "locale": "en_US",
                "given_name": "login at okta public local",
                "family_name": "test",
                "email": "login.at.okta.public.local@example.com"
              }
            }
          },
          {
            "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "SCOPE_email"
          },
          {
            "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "SCOPE_openid"
          },
          {
            "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "SCOPE_phone"
          },
          {
            "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
            "authority": "SCOPE_profile"
          }
        ]
      ],
      "idToken": {
        "@Class": "org.springframework.security.oauth2.core.oidc.OidcIdToken",
        "tokenValue": "actualTokenValueRemoved",
        "issuedAt": 1652411922.000000000,
        "expiresAt": 1652415522.000000000,
        "claims": {
          "@Class": "java.util.Collections$UnmodifiableMap",
          "at_hash": "X4TpvHXTMw3kaBMRas-H6A",
          "sub": "00u4zn2e0w8cRRFv75d1",
          "ver": [
            "java.lang.Long",
            1
          ],
          "amr": [
            "java.util.ArrayList",
            [
              "pwd"
            ]
          ],
          "iss": [
            "java.net.URL",
            "https://dev-231911394952.okta.com"
          ],
          "preferred_username": "login.at.okta.public.local",
          "nonce": "pWQDfOxvpslr_FWiTqHlIRdCiT5Sfq-PWvBmSyrDki0",
          "aud": [
            "java.util.ArrayList",
            [
              "0oa4varwxxjIMcDph5d7"
            ]
          ],
          "idp": "00otba6t5x5dSt78H5d6",
          "auth_time": [
            "java.time.Instant",
            1652411920.000000000
          ],
          "name": "login at okta public local test",
          "exp": [
            "java.time.Instant",
            1652415522.000000000
          ],
          "iat": [
            "java.time.Instant",
            1652411922.000000000
          ],
          "email": "login.at.okta.public.local@example.com",
          "jti": "ID.IfAKG8WvCxOy023DLl6I-WY-4pHgxMJZv1F79rqAtYM"
        }
      },
      "userInfo": {
        "@Class": "org.springframework.security.oauth2.core.oidc.OidcUserInfo",
        "claims": {
          "@Class": "java.util.Collections$UnmodifiableMap",
          "sub": "00u4zn2e0w8cRRFv75d1",
          "zoneinfo": "America/Los_Angeles",
          "email_verified": true,
          "updated_at": [
            "java.time.Instant",
            1652216788.000000000
          ],
          "name": "login at okta public local test",
          "preferred_username": "login.at.okta.public.local",
          "locale": "en_US",
          "given_name": "login at okta public local",
          "family_name": "test",
          "email": "login.at.okta.public.local@example.com"
        }
      },
      "nameAttributeKey": "sub"
    },
    "authorities": [
      "java.util.Collections$UnmodifiableRandomAccessList",
      [
        {
          "@Class": "org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority",
          "authority": "ROLE_USER",
          "idToken": {
            "@Class": "org.springframework.security.oauth2.core.oidc.OidcIdToken",
            "tokenValue": "actualTokenValueRemoved",
            "issuedAt": 1652411922.000000000,
            "expiresAt": 1652415522.000000000,
            "claims": {
              "@Class": "java.util.Collections$UnmodifiableMap",
              "at_hash": "X4TpvHXTMw3kaBMRas-H6A",
              "sub": "00u4zn2e0w8cRRFv75d1",
              "ver": [
                "java.lang.Long",
                1
              ],
              "amr": [
                "java.util.ArrayList",
                [
                  "pwd"
                ]
              ],
              "iss": [
                "java.net.URL",
                "https://dev-231911394952.okta.com"
              ],
              "preferred_username": "login.at.okta.public.local",
              "nonce": "pWQDfOxvpslr_FWiTqHlIRdCiT5Sfq-PWvBmSyrDki0",
              "aud": [
                "java.util.ArrayList",
                [
                  "0oa4varwxxjIMcDph5d7"
                ]
              ],
              "idp": "00otba6t5x5dSt78H5d6",
              "auth_time": [
                "java.time.Instant",
                1652411920.000000000
              ],
              "name": "login at okta public local test",
              "exp": [
                "java.time.Instant",
                1652415522.000000000
              ],
              "iat": [
                "java.time.Instant",
                1652411922.000000000
              ],
              "email": "login.at.okta.public.local@example.com",
              "jti": "ID.IfAKG8WvCxOy023DLl6I-WY-4pHgxMJZv1F79rqAtYM"
            }
          },
          "userInfo": {
            "@Class": "org.springframework.security.oauth2.core.oidc.OidcUserInfo",
            "claims": {
              "@Class": "java.util.Collections$UnmodifiableMap",
              "sub": "00u4zn2e0w8cRRFv75d1",
              "zoneinfo": "America/Los_Angeles",
              "email_verified": true,
              "updated_at": [
                "java.time.Instant",
                1652216788.000000000
              ],
              "name": "login at okta public local test",
              "preferred_username": "login.at.okta.public.local",
              "locale": "en_US",
              "given_name": "login at okta public local",
              "family_name": "test",
              "email": "login.at.okta.public.local@example.com"
            }
          }
        },
        {
          "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
          "authority": "SCOPE_email"
        },
        {
          "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
          "authority": "SCOPE_openid"
        },
        {
          "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
          "authority": "SCOPE_phone"
        },
        {
          "@Class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
          "authority": "SCOPE_profile"
        }
      ]
    ],
    "authorizedClientRegistrationId": "oktaDev",
    "details": {
      "@Class": "org.springframework.security.web.authentication.WebAuthenticationDetails",
      "remoteAddress": "127.0.0.1",
      "sessionId": "A8634EA2537A251E8B3D22A5CC4A6E3D"
    }
  },
  "org.springframework.security.oauth2.server.authorization.OAuth2Authorization.AUTHORIZED_SCOPE": [
    "java.util.Collections$UnmodifiableSet",
    [
      "openid"
    ]
  ]
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 18, 2022
@sjohnr
Copy link
Member

sjohnr commented May 19, 2022

Hi @kinsersh, welcome to the project!

It sounds like you're using Okta as your identity provider. Is this in the context of federated identity (similar to the federated identity sample)?

Is there a reason you can't add this mixin yourself as in, for example, this comment?

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels May 19, 2022
@sjohnr sjohnr self-assigned this May 19, 2022
@kinsersh
Copy link
Author

Hi @kinsersh, welcome to the project!

It sounds like you're using Okta as your identity provider. Is this in the context of federated identity (similar to the federated identity sample)?

Is there a reason you can't add this mixin yourself as in, for example, this comment?

Yes, I'm consuming Okta and others. I have added that mixin per that comment to work around this issue - it works well. I get it that Spring Authorization Server should stay free of project-specifics

Here are my reasons for submitting this pull request:

  1. Reduce friction for others that also want to integrate Okta with Spring Authorization Server, given how widespread Okta is used
  2. Avoid customizing the internals of Spring Authorization Server to simplify maintenance and improve future version compatibility. While customizing the Object Mapper isn't difficult, one does have to go through a few layers to get to it. If this pull request is included in the next version of Spring Authorization Server, I can remove my work around.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 19, 2022
@sjohnr
Copy link
Member

sjohnr commented May 19, 2022

Thanks for the feedback and your response @kinsersh! I see your points and while, as you mention, we want to stay free of embedding any customization needed for a specific use case in the core (for the time being), there are two sides to every coin such as increased friction and developer experience in this case.

We could open an issue to outline some general improvements to the OAuth2AuthorizationServerJackson2Module, which could include generic support for primitives/boxed types like long/Long. I will have to take this back and discuss it first, but it doesn't seem like we'd merge this as-is since it's too narrow a fix. I imagine we'd either fix it all, or not at all.

If this pull request is included in the next version of Spring Authorization Server, I can remove my work around.

It's a small detail, but in this case I'm not sure it's a workaround so much as a customization. So while we want to make customization possible, there are lots of examples where it will be required to customize, as that's the framework's entire purpose to some degree. Just wanted to clarify that point in case it's relevant later.

For now, I'll leave this open as a placeholder for that more general issue I want to open, once we've discussed it a bit.

@sjohnr sjohnr removed the status: feedback-provided Feedback has been provided label May 19, 2022
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 19, 2022
@kinsersh
Copy link
Author

kinsersh commented May 23, 2022

Thanks for the feedback and your response @kinsersh! I see your points and while, as you mention, we want to stay free of embedding any customization needed for a specific use case in the core (for the time being), there are two sides to every coin such as increased friction and developer experience in this case.

We could open an issue to outline some general improvements to the OAuth2AuthorizationServerJackson2Module, which could include generic support for primitives/boxed types like long/Long. I will have to take this back and discuss it first, but it doesn't seem like we'd merge this as-is since it's too narrow a fix. I imagine we'd either fix it all, or not at all.

If this pull request is included in the next version of Spring Authorization Server, I can remove my work around.

It's a small detail, but in this case I'm not sure it's a workaround so much as a customization. So while we want to make customization possible, there are lots of examples where it will be required to customize, as that's the framework's entire purpose to some degree. Just wanted to clarify that point in case it's relevant later.

For now, I'll leave this open as a placeholder for that more general issue I want to open, once we've discussed it a bit.

Sounds good. I'm all for a general fix. What I have here is a narrow fix - solves a compatibility issue with Okta right now, though what's the likelihood that Okta or another provider releases a change to their ID token format that uses a different primitive class for serializing/deserializing that's not yet trusted.

@jgrandja
Copy link
Collaborator

@kinsersh Thanks for all the details. As discussed, there are customization points that allow an application to configure serialization/deserialization for JSON types. Adding Long doesn't make sense for the framework since it is not used directly by the framework and would only be used for Okta specific flows.

Furthermore, adding this narrow change will open us to adding other narrow changes requested by users (in the future), and ultimately forcing us to maintain more code.

I see this as a minor customization on your end and I'm assuming there are other customizations required to federate Spring Authorization Server with Okta, so they all fall into that same group of customizations required to integrate with Okta.

Based on the explanation above, we cannot accept this change as it's not required for the framework specific code.

@jgrandja jgrandja closed this Jun 13, 2022
@jgrandja jgrandja added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged labels Jun 13, 2022
@jgrandja jgrandja assigned jgrandja and unassigned sjohnr Jun 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants