feat(identity): Temporary credentials jwt#6213
feat(identity): Temporary credentials jwt#6213salvatore-coppola wants to merge 5 commits intodevelopfrom
Conversation
1172437 to
d771e89
Compare
There was a problem hiding this comment.
Pull request overview
Adds JWT token-pair authentication alongside the existing session/password flow, enabling container identity integration and REST access via bearer tokens, backed by a new core IdentityTokenService implementation and config flags to enable/disable JWT support.
Changes:
- Introduces
IdentityTokenServiceAPI + core implementation for issuing/refreshing/verifying/revoking JWT token pairs and identity revision tracking. - Extends REST auth with JWT bearer provider and
/session/v2token endpoints + config toggleauth.jwt.enabled. - Updates container identity integration to provision tokens via secret files (with scheduled renewal) and adds
container.identity.auth.mode.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| kura/test/org.eclipse.kura.rest.provider.test/src/main/java/org/eclipse/kura/rest/provider/test/RestServiceTest.java | Adds REST tests for /session/v2 token endpoints and bearer auth. |
| kura/test/org.eclipse.kura.rest.cloudconnection.provider.test/src/main/java/org/eclipse/kura/internal/rest/cloudconnection/provider/test/CloudConnectionEndpointsTest.java | Adjusts pub/sub instance lifecycle tests and service tracking. |
| kura/test/org.eclipse.kura.container.provider.test/src/test/java/org/eclipse/kura/container/provider/ContainerIdentityIntegrationTest.java | Updates container identity integration tests to expect token-file injection instead of password env var. |
| kura/test-util/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/MqttTransport.java | Makes embedded MQTT broker startup/connection more resilient (dynamic port, reconnect logic). |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/provider/RestServiceOptions.java | Adds auth.jwt.enabled option handling. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/provider/RestService.java | Wires optional IdentityTokenService and conditionally registers JWT auth provider and token endpoints. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestServiceConstants.java | Introduces /session/v2 paths and refactors v1 constants under /session/v1. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestService.java | Adds /v2 login/refresh/logout token endpoints backed by IdentityTokenService. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/JwtAuthenticationProvider.java | New bearer-token AuthenticationProvider using IdentityTokenService.verifyAccessToken. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/dto/TokenAuthenticationResponseDTO.java | New response DTO for token-pair responses. |
| kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/dto/RefreshTokenDTO.java | New DTO for refresh-token requests. |
| kura/org.eclipse.kura.rest.provider/OSGI-INF/metatype/org.eclipse.kura.internal.rest.provider.RestService.xml | Adds metatype entry for auth.jwt.enabled. |
| kura/org.eclipse.kura.core.identity/src/main/java/org/eclipse/kura/core/identity/IdentityTokenServiceImpl.java | Implements JWT issuance/verification/refresh with refresh-token rotation tracking and keystore-backed signing. |
| kura/org.eclipse.kura.core.identity/src/main/java/org/eclipse/kura/core/identity/IdentityServiceImpl.java | Adds identity revision tracking + getIdentityRevision for token invalidation use cases. |
| kura/org.eclipse.kura.core.identity/META-INF/MANIFEST.MF | Adds imports needed by the token service implementation (keystore, Gson). |
| kura/org.eclipse.kura.container.provider/src/main/java/org/eclipse/kura/container/provider/ContainerInstanceOptions.java | Adds container.identity.auth.mode option (jwt/password). |
| kura/org.eclipse.kura.container.provider/src/main/java/org/eclipse/kura/container/provider/ContainerInstance.java | Provisions JWT tokens as files/volumes/env vars and schedules access-token renewal via refresh tokens. |
| kura/org.eclipse.kura.container.provider/OSGI-INF/metatype/org.eclipse.kura.container.provider.ContainerInstance.xml | Adds metatype entry for container.identity.auth.mode. |
| kura/org.eclipse.kura.container.provider/OSGI-INF/containerinstance.xml | Adds DS reference injection for IdentityTokenService. |
| kura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/VerifiedAccessToken.java | New API type returned by token verification. |
| kura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/TokenPair.java | New API type representing access/refresh tokens + expiry + family id. |
| kura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/IdentityTokenService.java | New API for issuing/refreshing/verifying/revoking tokens. |
| kura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/IdentityService.java | Adds getIdentityRevision API used for token invalidation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -231,6 +269,14 @@ private void updateBuiltinAuthenticationProviders(final RestServiceOptions optio | |||
| if (options.isSessionManagementEnabled()) { | |||
| bindAuthenticationProvider(this.sessionAuthenticationProvider); | |||
There was a problem hiding this comment.
updateBuiltinAuthenticationProviders binds sessionAuthenticationProvider when session.management.enabled is true, but never unbinds it when the option is later set to false. This makes session.management.enabled=false ineffective after a previous true state (sessions can still authenticate). Add an else branch that unbinds sessionAuthenticationProvider when session management is disabled (mirroring the basic/certificate handling).
| bindAuthenticationProvider(this.sessionAuthenticationProvider); | |
| bindAuthenticationProvider(this.sessionAuthenticationProvider); | |
| } else { | |
| unbindAuthenticationProvider(this.sessionAuthenticationProvider); |
| private void writeTokenFile(final Path tokenFile, final String tokenValue) throws IOException { | ||
| final Set<PosixFilePermission> permissions = Set.of(PosixFilePermission.OWNER_READ, | ||
| PosixFilePermission.OWNER_WRITE); | ||
|
|
||
| try { | ||
| Files.createFile(tokenFile, PosixFilePermissions.asFileAttribute(permissions)); | ||
| } catch (FileAlreadyExistsException e) { | ||
| Files.setPosixFilePermissions(tokenFile, permissions); | ||
| } catch (UnsupportedOperationException e) { | ||
| logger.debug("POSIX permissions not supported for {}", tokenFile, e); | ||
| } | ||
|
|
||
| Files.write(tokenFile, tokenValue.getBytes(StandardCharsets.UTF_8), | ||
| StandardOpenOption.TRUNCATE_EXISTING); | ||
| } |
There was a problem hiding this comment.
writeTokenFile swallows UnsupportedOperationException from Files.createFile(..., PosixFilePermissions.asFileAttribute(...)), but then immediately calls Files.write(..., TRUNCATE_EXISTING). On non-POSIX file systems this leaves the file uncreated and Files.write will fail with NoSuchFileException. Also, in the FileAlreadyExistsException branch Files.setPosixFilePermissions(...) can throw UnsupportedOperationException and currently isn’t handled. Consider writing with CREATE/TRUNCATE_EXISTING and applying POSIX permissions in a separate best-effort block so token provisioning works across supported file systems.
| final JsonObject header = parseJsonObject(parts[0]); | ||
| final JsonObject payload = parseJsonObject(parts[1]); | ||
|
|
||
| final String algorithm = payloadValue(header, "alg"); | ||
| if (!JWT_ALG.equals(algorithm)) { | ||
| throw new KuraException(KuraErrorCode.SECURITY_EXCEPTION, "Unsupported JWT algorithm"); | ||
| } | ||
|
|
||
| final String kid = payloadValue(header, "kid"); | ||
| final String signingInput = parts[0] + "." + parts[1]; | ||
| verifySignature(signingInput, parts[2], getVerificationPublicKey(kid)); | ||
|
|
||
| validateClaims(payload); | ||
|
|
||
| return new DecodedJwt(payload); |
There was a problem hiding this comment.
decodeAndVerify can throw unchecked exceptions (e.g., invalid base64url / invalid JSON in parseJsonObject, or JsonSyntaxException) that are not wrapped as KuraException. This is particularly problematic for refreshTokenPair, which only declares/handles KuraException, and for the REST refresh endpoint which catches KuraException only—malformed tokens could surface as 500s instead of a clean 401/invalid-token response. Catch and wrap decode/parse errors into a KuraException with SECURITY_EXCEPTION (e.g., “Malformed JWT”).
| final JsonObject header = parseJsonObject(parts[0]); | |
| final JsonObject payload = parseJsonObject(parts[1]); | |
| final String algorithm = payloadValue(header, "alg"); | |
| if (!JWT_ALG.equals(algorithm)) { | |
| throw new KuraException(KuraErrorCode.SECURITY_EXCEPTION, "Unsupported JWT algorithm"); | |
| } | |
| final String kid = payloadValue(header, "kid"); | |
| final String signingInput = parts[0] + "." + parts[1]; | |
| verifySignature(signingInput, parts[2], getVerificationPublicKey(kid)); | |
| validateClaims(payload); | |
| return new DecodedJwt(payload); | |
| try { | |
| final JsonObject header = parseJsonObject(parts[0]); | |
| final JsonObject payload = parseJsonObject(parts[1]); | |
| final String algorithm = payloadValue(header, "alg"); | |
| if (!JWT_ALG.equals(algorithm)) { | |
| throw new KuraException(KuraErrorCode.SECURITY_EXCEPTION, "Unsupported JWT algorithm"); | |
| } | |
| final String kid = payloadValue(header, "kid"); | |
| final String signingInput = parts[0] + "." + parts[1]; | |
| verifySignature(signingInput, parts[2], getVerificationPublicKey(kid)); | |
| validateClaims(payload); | |
| return new DecodedJwt(payload); | |
| } catch (final RuntimeException e) { | |
| throw new KuraException(KuraErrorCode.SECURITY_EXCEPTION, "Malformed JWT"); | |
| } |
| @Override | ||
| public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { | ||
| switch (method.getName()) { | ||
| case "issueTokenPair": | ||
| return issueTokenPair((String) args[0]); | ||
| case "refreshTokenPair": | ||
| return refreshTokenPair((String) args[0]); | ||
| case "verifyAccessToken": | ||
| return verifyAccessToken((String) args[0]); | ||
| case "revokeTokenFamily": | ||
| revokeTokenFamily((String) args[0]); | ||
| return null; | ||
| default: | ||
| throw new UnsupportedOperationException("Unsupported method: " + method.getName()); | ||
| } |
There was a problem hiding this comment.
The InvocationHandler proxy used for the test IdentityTokenService only handles the service API methods. Calls to toString(), hashCode(), or equals() on the proxy will currently hit the default case and throw UnsupportedOperationException, which can break OSGi/service registry interactions or logging that implicitly calls these methods. Handle Object methods explicitly (or treat unknown methods as no-ops) so the proxy is safe to use as an OSGi service.
| } catch (final Exception e) { | ||
| stopMoquetteBroker(); | ||
| initialized.set(false); | ||
| throw new RuntimeException(e); | ||
| } |
There was a problem hiding this comment.
In init(), the exception handler no longer calls stopMoquetteBroker(). If initialization fails after startMoquetteBroker() succeeds, the broker remains running and mqttBrokerPort stays allocated, which can cause flaky tests and resource leakage across runs. Consider stopping the broker (and resetting mqttBrokerPort if needed) in the failure path, as was previously done.
| return new TokenAuthenticationResponseDTO(tokenPair, false); | ||
| } catch (final KuraException e) { | ||
| if (e.getCode() == KuraErrorCode.SECURITY_EXCEPTION) { | ||
| throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, e.getMessage()); |
There was a problem hiding this comment.
refreshTokenPair maps KuraErrorCode.SECURITY_EXCEPTION to HTTP 401 while passing e.getMessage() through to the client. Since IdentityTokenServiceImpl can produce detailed messages (e.g., missing claim names, signature/validation failures), this can unnecessarily leak validation details and create a token-oracle style distinction. Consider returning a fixed message like “Invalid refresh token” for all security exceptions while still logging the detailed cause server-side.
| throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, e.getMessage()); | |
| LoggerFactory.getLogger(SessionRestService.class).warn("Refresh token validation failed", e); | |
| throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, | |
| "Invalid refresh token"); |
9303a0d to
6a75649
Compare
Signed-off-by: MMaiero <matteo.maiero@eurotech.com>
…en error mapping Disable the broken IdentityTokenServiceImpl ServiceFactory component before registering the test IdentityTokenService to prevent the poisoned 0..1 SCR reference from blocking service binding. Reorder the shouldSupportDisablingBuiltInJwtAuthentication test to login before disabling JWT auth. Return 401 instead of 500 for SECURITY_EXCEPTION in the token refresh endpoint.
- Use AtomicBoolean.compareAndSet for refresh token rotation to eliminate the race condition window between check and mark-as-used - Track revoked token family expiry and prune in cleanup to prevent unbounded memory growth on long-running embedded devices - Route all JWT claim reads through null-safe helpers to prevent NullPointerException on malformed tokens missing expected claims - Remove dead isExpired guard from revocation check in refreshTokenPair - Add audit logging to JWT logout endpoint matching the v1 pattern - Create token files with restricted POSIX permissions atomically to close the TOCTOU window where tokens are briefly world-readable - Derive token renewal interval from actual access token lifetime instead of using a hardcoded 60-second interval
6a75649 to
f819df2
Compare
This pull request introduces support for JWT-based authentication for container identity integration, alongside the existing password-based approach. It adds new interfaces and data structures for token management, updates the container provider to handle tokens, and enhances configuration options to allow selection of authentication mode. The most important changes are grouped below:
JWT Token-based Authentication Support:
IdentityTokenServiceinterface,TokenPair, andVerifiedAccessTokenclasses to the API for issuing, refreshing, verifying, and revoking JWT tokens. (kura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/IdentityTokenService.java[1]TokenPair.java[2]VerifiedAccessToken.java[3]ContainerInstanceto support JWT authentication: provisions JWT tokens as files, injects relevant environment variables and volumes, and manages token lifecycle (including renewal scheduling and cleanup). (ContainerInstance.java[1] [2] [3] [4] [5] [6]Configuration and Dependency Injection:
container.identity.auth.modeto select betweenjwtandpasswordauthentication modes, defaulting tojwt. (OSGI-INF/metatype/org.eclipse.kura.container.provider.ContainerInstance.xmlkura/org.eclipse.kura.container.provider/OSGI-INF/metatype/org.eclipse.kura.container.provider.ContainerInstance.xmlR197-R200)IdentityTokenServicedependency in the container provider's OSGi XML and Java class. (OSGI-INF/containerinstance.xml[1]ContainerInstance.java[2]API Enhancements:
getIdentityRevisionmethod toIdentityServiceto retrieve the current revision of an identity, supporting token invalidation and refresh scenarios. (IdentityService.javakura/org.eclipse.kura.api/src/main/java/org/eclipse/kura/identity/IdentityService.javaR281-R294)These changes collectively enable secure, flexible authentication for containers, with a focus on modern token-based workflows and improved configurability.