Skip to content

OIDC: NPE when accessing IdToken when Bearer access token is sent #35964

Closed
@tmulle

Description

Describe the bug

I'm testing out the multitenant solution in my application so I can support both code-flow and bearer token for my JSF UI and REST API code. I want the REST endpoints to be accessible from curl, postman, etc. so I can't use the hybrid type.

My JSF ui uses the web-app service type and my REST apis use the service type.

Things seem to be working ok and I can log in fine in the UI and also use postman to hit my rest endpoints passing an access token in the Authorization header.

However, I have a API method that I wanted to print out the token information that is available to me so I can learn what is actually provided during which calls.

I think I found a bug, in JsonWebToken and OidcSession when trying to access expirationDate.

Error:

2023-09-15 16:55:21,426 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /api/tokeninfo failed, error id: 225fcb1e-3b82-4731-aec2-b982c3a26469-3: java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "org.eclipse.microprofile.jwt.JsonWebToken.getClaim(org.eclipse.microprofile.jwt.Claims)" is null
        at org.eclipse.microprofile.jwt.JsonWebToken.getExpirationTime(JsonWebToken.java:98)
        at org.eclipse.microprofile.jwt.OidcJsonWebTokenProducer_ProducerMethod_currentIdToken_fd05e4c440bc560635edfdf219600734450212ad_ClientProxy.getExpirationTime(Unknown Source)
        at org.primefaces.babylon.service.SecurityService.printTokenInfo(SecurityService.java:80)
        at org.primefaces.babylon.service.SecurityService_Subclass.printTokenInfo$$superforward(Unknown Source)
        at org.primefaces.babylon.service.SecurityService_Subclass$$function$$12.apply(Unknown Source)
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
        at io.quarkus.arc.impl.AroundInvokeInvocationContext$NextAroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:97)

Below is the service I am hitting from both the UI and the REST end, and the UI calls via JSF work fine, but the Postman doesn't.

It looks like there is some sort of proxy object calls JsonNullWebToken or something that causing the initial null check to pass but then internally it blows up trying to access other information.

This happens for both the OidcSession and the JsonWebToken idToken.

Since I'm hitting the API with an AccessToken only, I would assume the IdToken would be null, but it is not. Which is why I put the null check in there to try to fix it.

When I call the same method via JSF #{securityService.printTokenInfo()} I see all the information print out which I expect.

@Named
@ApplicationScoped
@PermitAll
public class SecurityService implements Serializable {

    @Inject
    Logger log;

    @Inject
    SecurityIdentity identity;

    @Inject
    @IdToken
    JsonWebToken idToken;

    @Inject
    JsonWebToken accessToken;

    @Inject
    OidcSession session;

    public boolean isLoggedIn() {
        log.infof("User %s is logged in = %s and has roles %s", identity.getPrincipal().getName(), !identity.isAnonymous(),identity.getRoles() );
        return identity != null && !identity.isAnonymous();
    }

    public boolean hasRole(String role) {
        return identity.hasRole(role);
    }


    public void printTokenInfo() {
        Instant now = Instant.now();

        Instant accessTokenExpiration = null;
        Instant idTokenExpiration = null;
        Duration accessTokenDuration = null;
        Duration idTokenDuration = null;
        
        if (accessToken != null) {
            accessTokenExpiration = Instant.ofEpochSecond(accessToken.getExpirationTime());
            accessTokenDuration = Duration.between(now, accessTokenExpiration);

            log.infof("Access Token Duration is %d days, %d hours, %d minutes, and %d seconds.\n",
                    accessTokenDuration.toDays(),
                    accessTokenDuration.toHoursPart(),
                    accessTokenDuration.toMinutesPart(),
                    accessTokenDuration.toSecondsPart());
        } 
        
        if (idToken != null) {
            idTokenExpiration = Instant.ofEpochSecond(idToken.getExpirationTime());
            idTokenDuration = Duration.between(now, idTokenExpiration);

            log.infof("Info Token Duration is %d days, %d hours, %d minutes, and %d seconds.\n",
                    idTokenDuration.toDays(),
                    idTokenDuration.toHoursPart(),
                    idTokenDuration.toMinutesPart(),
                    idTokenDuration.toSecondsPart());
        }


        if (session != null && session.getIdToken() != null) {
            log.infof("Session information for tenant [%s] with username [%s] expires at: [%s] and is valid for [%s] - Groups: %s Claims: %s",
                    session.getTenantId(), session.getIdToken().getName(),
                    session.expiresAt(), session.validFor(),
                    session.getIdToken().getGroups(), session.getIdToken().getClaimNames());
        }
    }

    @Path("/api/tokeninfo")
    @GET
    @RolesAllowed("web-license-admin")
    @Produces(MediaType.TEXT_PLAIN)
    public Response doNothing() {
        printTokenInfo();
        return Response.ok().build();
    }
}```


### Expected behavior

_No response_

### Actual behavior

_No response_

### How to Reproduce?

_No response_

### Output of `uname -a` or `ver`

_No response_

### Output of `java -version`

_No response_

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

3.3.2

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

_No response_

### Additional information

_No response_

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions