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

No documentation on how to implement custom JWT validation #1851

Open
distinctdan opened this issue Nov 7, 2024 · 8 comments
Open

No documentation on how to implement custom JWT validation #1851

distinctdan opened this issue Nov 7, 2024 · 8 comments

Comments

@distinctdan
Copy link

Expected Behavior

It looks like all the necessary classes exist in micronaut-security, but I can't find a single piece of documentation on how to use them correctly. JWT validation is common and token formats vary a lot, so I would expect this to come up a lot. Am I missing something, or is this undocumented?

Here's more information about my use case and the things I've looked at:

  • Our token format: we don't have a simple roles property, instead we have a map with additional information for each role, like the locations for which the user has that role. I can generate a roles list from the token, but I'm not sure where to put the logic. The docs briefly mention JsonWebTokenParser, but it only outputs claims, not roles?
  • Signature verification: The docs say to use ReactiveJsonWebTokenValidator but provide no information about how to do that. I'm also surprised this isn't on by default, or at least the docs sound like you have to do additional work to make it verify. If signatures aren't verified, then there isn't any security.
  • Custom Authentication class. I need to parse the token once to get the claims, then pass the parsed object down through my controller and services layer to do security checks. The Authentication class is loosely typed and doesn't appear to support generics. I'm using Kotlin, and I would consider strong typing of the claims to be a requirement here.

I hope I'm just missing things here, is there a more advanced guide available that fully walks through how to set up custom JWT auth? Thanks.

Actual Behaviour

No response

Steps To Reproduce

No response

Environment Information

No response

Example Application

No response

Version

4.6.3

@distinctdan
Copy link
Author

Thanks, yes, I read those and the Okta guide, but none of them contained the information I needed. For our server, we don't care about generating tokens because we use 3rd party auth, we just need to validate custom claims. After a lot of digging, I finally just gave up and cloned the micronaut-security repo and had to read through the source. After reading through it, I figured out a few key points that I needed to do which weren't in any documentation I could find:

  • Subclass Authentication so that we can have additional properties like out rolesToLocations map, we called this AuthenticatedEntity.
  • Implement TokenValidator and manually parse the token so that we can pull out the custom claims, flatten them to roles, and store them both in our Authentication subclass. Technically this could have been done using the attributes map of Authentication, but then all the consumers will have to cast it, so a custom subclass is cleaner. I don't like doing this because I don't want to have to learn all the correct Nimbus configurations to set up the validator, but maybe that's for the best anyways because we do want it to only allow the things that Okta supports and nothing else. I still need to make sure we have kid rate limiting turned on and to write tests to make sure it's actually pulling keys from the issuer. It would be a lot better if micronaut provided a way to do just handle the claims and Authentication creation without having to fully implement a custom validator.
  • Implement TypedRequestArgumentBinder<AuthenticatedEntity> to allow us to inject our custom Authentication class into the controller so that we can use our additional properties for security checks. The docs mentioned this, but didn't provide any details on how binding works, but I eventually found some binding docs buried in the source.

For anyone else who runs across this, here's my current implementation:

@Singleton
class CfaTokenValidator<R> @Inject constructor(
    @Property(name = "cfa-token-validator.issuer")
    val expectedIssuer: String,

    @Property(name = "cfa-token-validator.jwks-keys-url")
    val jwksKeysUrl: String,
) : TokenValidator<R> {

    private val LOG = LoggerFactory.getLogger(CfaTokenValidator::class.java)

    private val jwtProcessor: ConfigurableJWTProcessor<SecurityContext> = DefaultJWTProcessor<SecurityContext>().apply {
        // Configure the JWT processor with a key selector to feed matching public
        // RSA keys sourced from the JWK set URL
        setJWSKeySelector(
            JWSVerificationKeySelector(
                JWSAlgorithm.RS256,
                // TODO - Configure retries and expiration
                // The public RSA keys to validate the signatures will be sourced from the
                // OAuth 2.0 server's JWK set URL. The key source will cache the retrieved
                // keys for 5 minutes. 30 seconds prior to the cache's expiration the JWK
                // set will be refreshed from the URL on a separate dedicated thread.
                // Retrial is added to mitigate transient network errors.
                JWKSourceBuilder
                    .create<SecurityContext>(URL(jwksKeysUrl))
                    .retrying(true)
                    .build()
            )
        )

        // Set the required JWT claims for access tokens
        setJWTClaimsSetVerifier(
            DefaultJWTClaimsVerifier(
                JWTClaimsSet.Builder().issuer(expectedIssuer).build(),
                HashSet(
                    Arrays.asList(
                        JWTClaimNames.SUBJECT,
                        JWTClaimNames.ISSUED_AT,
                        JWTClaimNames.EXPIRATION_TIME,
                        "scp",
                        "cid",
                        JWTClaimNames.JWT_ID
                    )
                )
            )
        )
    }

    @SingleResult
    override fun validateToken(token: String, request: R?): Publisher<Authentication> {
        // Parse the token. We're returning AuthenticatedEntity, which subclasses Authentication. We're using an
        // ArgumentBinder to be able to inject it as an AuthenticatedEntity in the controller.
        val claimsSet = jwtProcessor.process(token, null)
        return Mono.just(AuthenticatedUser.fromClaimsSet(claimsSet))
    }
}

/**
 * Allows us to use a custom Authentication class in controllers. This is partially copied from:
 * https://micronaut-projects.github.io/micronaut-security/latest/guide/#customAuthenticatedUser
 *
 * Binding docs here:
 * https://github.com/micronaut-projects/micronaut-core/blob/d1ac9b86298bd4c242064dd3de1ab24c9506be98/src/main/docs/guide/httpServer/customArgumentBinding.adoc
 */
@Singleton
class AuthenticatedUserArgumentBinder : TypedRequestArgumentBinder<AuthenticatedUser> {
    override fun bind(
        context: ArgumentConversionContext<AuthenticatedUser>,
        request: HttpRequest<*>,
    ): ArgumentBinder.BindingResult<AuthenticatedUser> {
        // Binders can be called multiple times at different stages of the request process. We need the security filter
        // to run first before we can get access to the Authentication.
        if (!request.attributes.contains(SecurityFilter.KEY)) {
            // This will cause us to possibly be called again later in the binding process.
            return ArgumentBinder.BindingResult.unsatisfied()
        }

        val authOpt = request.getUserPrincipal(Authentication::class.java)
        // If Authentication isn't null, try casting it to AuthenticatedEntity, which subclasses Authentication.
        val auth = authOpt.orElse(null)?.let { it as? AuthenticatedUser }

        // Since the security filter has already run, we can return a final binding result.
        return if (auth != null) {
            ArgumentBinder.BindingResult { Optional.of(auth) }
        } else ArgumentBinder.BindingResult.empty()
    }

    override fun argumentType(): Argument<AuthenticatedUser> {
        return Argument.of(AuthenticatedUser::class.java)
    }
}

@distinctdan
Copy link
Author

I also found the NimbusReactiveJsonWebTokenValidator. It's pretty heavily abstracted which made it a bit hard to understand, but it was useful in seeing which pieces of the puzzle I needed to handle. I see it does some custom claims validation, but I wanted to just rely on the built-in @Secured validation which relies on the Authentication.getRoles property. Also, I wanted to be sure that our claims are only parsed once. In the NimbusReactive one, it passes the JWT to validateClaims. Since we need to do some transformation of the JWT to pull out our custom claims and flatten them, for maximum performance I don't want to do that twice, so I would rather pass an Authentication to validate against so that we only have to create it once.

@sdelamo
Copy link
Contributor

sdelamo commented Nov 22, 2024

@distinctdan thanks for getting back. Will you be willing to contribute a PR to improve the documentation?

@ArthurHarkivsky
Copy link

@sdelamo Hello! Could an example for RSAEncryptionConfiguration be added also?

@distinctdan
Copy link
Author

distinctdan commented Dec 2, 2024

Yeah, I could be down to add some docs. Here's what I'm thinking as an outline, I would welcome feedback to see if this is in line with what you're thinking:

  • Add 2 sections in between 10.3.2 and 10.3.3 called Custom TokenValidation and Subclassing Authentication.
  • Include the above code samples, but simplified to the minimum example and with generic class names.
  • For the binding documentation, I would have an example of how to use this for Authentication and link to the existing docs here https://docs.micronaut.io/4.7.6/guide/#customArgumentBinding.

Also, if you have a better way to handle this, I'm open to that too, just wanted to check in.

@distinctdan
Copy link
Author

@ArthurHarkivsky Sorry I don't know anything about that, but maybe you or someone else would be able to add them.

@distinctdan
Copy link
Author

^ Edit: The existing docs do cover the meaning of BindingResult, I had missed that initially, so I think just adding the example of how to use them for Authentication will be fine without updating any docs in the main repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

3 participants