-
Notifications
You must be signed in to change notification settings - Fork 128
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
Comments
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:
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)
}
} |
I also found the |
@distinctdan thanks for getting back. Will you be willing to contribute a PR to improve the documentation? |
@sdelamo Hello! Could an example for RSAEncryptionConfiguration be added also? |
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:
Also, if you have a better way to handle this, I'm open to that too, just wanted to check in. |
@ArthurHarkivsky Sorry I don't know anything about that, but maybe you or someone else would be able to add them. |
^ 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 |
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:
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 mentionJsonWebTokenParser
, but it only outputs claims, not roles?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.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. TheAuthentication
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
The text was updated successfully, but these errors were encountered: