{"record":{"id":"6dd3c41ada4f1ee1","repo":"apache/pulsar","slug":"authentication-has-not-completed","errorCode":null,"errorMessage":"Authentication has not completed","messagePattern":"Authentication has not completed","errorType":"exception","errorClass":"AuthenticationException","httpStatus":null,"severity":"error","filePath":"pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/AuthenticationStateOpenID.java","lineNumber":54,"sourceCode":"    private AuthenticationDataSource authenticationDataSource;\n    private volatile String role;\n    private final SocketAddress remoteAddress;\n    private final SSLSession sslSession;\n    private volatile long expiration;\n\n    AuthenticationStateOpenID(\n            AuthenticationProviderOpenID provider,\n            SocketAddress remoteAddress,\n            SSLSession sslSession) {\n        this.provider = provider;\n        this.remoteAddress = remoteAddress;\n        this.sslSession = sslSession;\n    }\n\n    @Override\n    public String getAuthRole() throws AuthenticationException {\n        if (role == null) {\n            throw new AuthenticationException(\"Authentication has not completed\");\n        }\n        return role;\n    }\n\n    @Deprecated\n    @Override\n    public AuthData authenticate(AuthData authData) throws AuthenticationException {\n        // This method is not expected to be called and is subject to removal.\n        throw new AuthenticationException(\"Not supported\");\n    }\n\n    @Override\n    public CompletableFuture<AuthData> authenticateAsync(AuthData authData) {\n        final String token = new String(authData.getBytes(), UTF_8);\n        this.authenticationDataSource = new AuthenticationDataCommand(token, remoteAddress, sslSession);\n        return provider\n                .authenticateTokenAsync(authenticationDataSource)\n                .thenApply(jwt -> {","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/AuthenticationStateOpenID.java#L36-L72","documentation":"AuthenticationStateOpenID.getAuthRole() returns the authenticated role extracted from the validated JWT, which is stored only after authenticateAsync() successfully validates the token. If getAuthRole() is called before authentication has completed (role is still null), the state throws AuthenticationException(\"Authentication has not completed\"). It is a lifecycle/ordering guard: the broker framework (or custom code) asked for the authenticated identity too early in the connection handshake.","triggerScenarios":"Calling getAuthRole() on an AuthenticationStateOpenID instance before authenticateAsync() has completed successfully — e.g. calling it during the initial connection setup before the client sent its token, calling it after a failed or still-pending token validation future, or calling it synchronously while the async validation is still in flight.","commonSituations":"Broker plugins or protocol handlers that call getAuthRole() before checking AuthenticationState.isComplete(); a client that connects but never sends a Bearer token, so validation never runs; slow IdP discovery/JWKS fetch delaying completion while the broker polls for the role; a failed JWT validation leaving role null and then a retry path reading the role anyway.","solutions":["Ensure the authentication handshake completes before reading the role: check state.isComplete() first, and only then call getAuthRole().","Make sure the client is actually sending its token (Authorization: Bearer <token> / authData) so authenticateAsync() runs and sets the role.","If orchestrating manually, wait for the CompletableFuture returned by authenticateAsync() to complete before calling getAuthRole().","Verify token validation is not failing silently upstream (check broker logs for JWT/JWKS/issuer errors that leave role null)."],"exampleFix":"// before\nString role = authState.getAuthRole();\n// after\nif (authState.isComplete()) {\n    String role = authState.getAuthRole();\n} else {\n    // wait for authenticateAsync() to finish or reject the connection\n}","handlingStrategy":"validation","validationCode":"if (authState.isComplete()) {\n    String role = authState.getAuthRole();\n} else {\n    // defer: wait for authenticateAsync() to finish before reading the role\n}","typeGuard":"boolean hasAuthRole(AuthenticationState state) {\n    return state.isComplete(); // role != null for AuthenticationStateOpenID\n}","tryCatchPattern":"try {\n    String role = authState.getAuthRole();\n} catch (AuthenticationException e) {\n    // authentication not finished: re-run handshake or reject connection\n}","preventionTips":["Always gate getAuthRole() behind isComplete().","Drive authentication exclusively through authenticateAsync() and read the role in its thenApply/thenAccept callback.","Log/reject connections that reach role lookup without a completed handshake — it signals a client that never sent a token."],"tags":["authentication","oidc","lifecycle","java"],"backgroundTag":"authentication-not-completed","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}