stytchauth/stytch-java

Custom claims in Kotlin returns map with null values

Closed this issue · 5 comments

We have a custom claims template as follows

{
  "role": {{ user.trusted_metadata.role }},
  "artistProfileId": {{ user.trusted_metadata.artistProfileId }},
  "organisationRoles": {{ user.trusted_metadata.organisationRoles }},
  "signupType": {{ user.trusted_metadata.signupType }}
}

When we authenticate a user, we use the custom claims to pull the data we need in our application. Below is a full working example of creating a user, creating a magic link token, authenticating the user with that token and getting the custom claims.

// Creating the user via the API. Note we only set the role attribute here
val createUserResponse = StytchClient.users
    .createCompletable(CreateRequest(email = randomEmail(), trustedMetadata = mapOf("role" to "USER")))
    .get() as StytchResult.Success<CreateResponse>

val userId = createUserResponse.value.userId

// Create a magic link token
val magicLinkResponse = StytchClient.magicLinks
    .createCompletable(com.stytch.java.consumer.models.magiclinks.CreateRequest(userId = userId))
    .get() as StytchResult.Success<com.stytch.java.consumer.models.magiclinks.CreateResponse>

val token = magicLinkResponse.value.token

// Use the token to authenticate the user
val authResponse = StytchClient.magicLinks
    .authenticateCompletable(MagicLinksRequest(token = token, sessionDurationMinutes = 5))
    .get() as StytchResult.Success<AuthenticateResponse>

println(authResponse.value.session!!.customClaims)

Since we're only setting the role here, we expect the custom claims to come back only with that attribute, however, the code above prints the following:

{artistProfileId=null, organisationRoles=null, role=USER, signupType=null}

In other words, all the custom claims are present, even when the trusted metadata doesn't have any value (and are null in that case).

This wouldn't be a big issue except for the fact that the Kotlin API exposes custom claims as a Map<String, Any>?:

The values in the map are expected to be non-null, but claims fail that contract, and in the Kotlin world this is a breaking issue. In the example above if we try to read the custom claims, relying on the Map<String,Any> contract, the application will fail at runtime because it will find nulls in a map that, according to compile time checks, shouldn't contain nulls.

Another issue here is the Map<String, Any>? contract which would be preferable as Map<String, Any>, and just return an empty map when there are no claims. I'm not sure if this is required of Java-interop or if it's just an oversight.

Just added this to our cycle, I'll update when it's fixed.

Thank you as always for your detailed reports!

Hey @cosmin-marginean,

The SDK is codegened, so there are some limits to what we can do in terms of specifying typings and knowing which fields are nullable or not. That said, I made the following tweaks (in #26 ):

  1. All Map<String, Any>? are now Map<String, Any?>?, so you shouldn't have any runtime crashes when parsing a map with a null value. Unfortunately, the nullability of the property is still there (a limitation of our protobuf parsing)
  2. All default values for maps and lists are now emptyMap() or emptyList()

Do these changes seem like they will work for you?

Yes, that makes sense, at least knowing that the values can be null at compile time (point 1) is helpful.
Thank you!

Re: point 2, I'm not sure if that helps as long as the map is marked as nullable (Map<String, Any?>? instead of Map<String, Any?>) but it's good to know at least

Alright, I'm going to go ahead and release that PR, and thanks again for reporting!

3.0.0 released (may take an hour or so to show up on Maven)