How do I load a "user" in a micronaut backend when JWT is provided

3.8k Views Asked by At

I have a Micronaut microservice that handles authentication via JsonWebTokens (JWT) from this guide.

Now I'd like to extend this code. The users in my app have some extra attributes such as email, adress, teamId etc. I have all users in the database.

How do I know in the backend controller method which user corresponds to the JWT that is sent by the client?

The guide contains this example code for the Micronaut REST controller:

@Secured(SecurityRule.IS_AUTHENTICATED)
@Controller
public class HomeController {
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    public String index(Principal principal) {
        return principal.getName();
    }
}

I know that I can get the name of the principal, ie. the username from the HttpRequest. But how do I get my additional attributes?

(Maybe I misunderstand JWT a bit???)

  • Are these JWT "claims" ?
  • Do I need to load the corresponding user by username from my DB table?
  • How can I verify that the sent username is actually valid?

edit Describing my usecase in more detail:

Security requirements of my use case

  • Do not expose valid information to the client
  • Validate everything the client (a mobile app) sends via REST

Authentication Flow

default oauth2 flow with JWTs:

Precondition: User is already registerd. Username, hash(password) and furhter attributes (email, adress, teamId, ..) are known on the backend.

  1. Client POSTs username and password to /login endpoint
  2. Client receives JWT in return, signed with server secret
  3. On every future request the client sends this JWT as bearer in the Http header.
  4. Backend validates JWT <==== this is what I want to know how to do this in Micronaut.

Questions

  • How to validate that the JWT is valid?
  • How to and where in which Java class should I fetch additional information for that user (the additional attributes). What ID should I use to fetch this information. The "sub" or "name" from the decoded JWT?
3

There are 3 best solutions below

4
On BEST ANSWER

How do I load a “user” in a micronaut backend when JWT is provided?

I am reading this as you plan to load some kind of User object your database and access it in the controller. If this is the case you need to hook into the place where Authentication instance is created to read the "sub" (username) of the token and then load it from the database.

How to extend authentication attributes with more details ?

By default for JWT authentication is created using JwtAuthenticationFactory and going more concrete default implementation is DefaultJwtAuthenticationFactory. If you plan to load more claims this could be done by replacing it and creating extended JWTClaimsSet or your own implementation of Authentication interface.

How do I access jwt claims ?

You need to check SecurityService -> getAuthentication() ->getAttributes(), it returns a map of security attributes which represent your token serialised as a map.

How to validate that the JWT is valid?

There is a basic validation rules checking the token is not expired and properly signed, all the rest validations especially for custom claims and validating agains a third parties sources have to be done on your own.

If you plan to validate your custom claims, I have already open source a project in this scope, please have a look.

https://github.com/traycho/micronaut-security-attributes

How to extend existing token with extra claims during its issuing ?

It is required to create your own claims generator extending JWTClaimsSetGenerator

@Singleton
@Replaces(JWTClaimsSetGenerator)
class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {

    CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration, @Nullable JwtIdGenerator jwtIdGenerator, @Nullable ClaimsAudienceProvider claimsAudienceProvider) {
        super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider)
    }

    protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
        super.populateWithUserDetails(builder, userDetails)

        // You your custom claims here
        builder.claim('email', userDetails.getAttributes().get("email"));

    }
}
0
On

How do I access jwt claims ?

If you want to access them from the rest handler just add io.micronaut.security.authentication.Authentication as an additional parameter in the handling method. Example

@Get("/{fooId}")
@Secured(SecurityRule.IS_AUTHENTICATED)
public HttpResponse<Foo> getFoo(long fooId, Authentication authentication) {
    ...
}
0
On

I found a solution. The UserDetails.attributes are serialized into the JWT. And they can easily be set in my CustomAuthenticationProviderclass:

@Singleton
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Publisher<AuthenticationResponse> authenticate(
        @Nullable HttpRequest<?> httpRequest, 
        AuthenticationRequest<?, ?> authenticationRequest) 
    {
        // ... autenticate the request here ...
        // eg. via BasicAuth or Oauth 2.0 OneTimeToken
        // then if valid:

        return Flowable.create(emitter -> {
            UserDetails userDetails = new UserDetails("sherlock", Collections.emptyList(), "[email protected]");

            // These attributes will be serialized as custom claims in the JWT
            Map attrs = CollectionUtils.mapOf("email", email, "teamId", teamId)
            userDetails.setAttributes(attrs);

            emitter.onNext(userDetails);
            emitter.onComplete();
        }, BackpressureStrategy.ERROR);
    }
}

And some more pitfalls when validating the JWT in the backend

A JWT in Micronaut MUST contain a "sub" claim. The JWT spec does not require this, but Micronaut does. The value of the "sub" claim will become the username of the created UserDetails object.

If you want to load addition attributes into these UserDetails when the JWT is validated in the backend, then you can do this by implementing a TokenValidator. But (another pitfal) then you must set its ORDER to a value larger than micronaut's JwtTokenValidator. Your order must be > 0 otherwise your TokenValidator will not be called at all.