Integrate in Existing Java Project
Add Hawcx authentication to an existing Java application
Integrating Hawcx Backend SDK into Existing Java Projects
This guide walks through adding Hawcx OAuth authentication to an existing Java backend (Spring Boot is shown, but the same pattern works for any framework).
Step 1: Add the SDK Dependency
The SDK is on Maven Central. Java 21+ is required.
Maven:
<dependency>
<groupId>com.hawcx</groupId>
<artifactId>hawcx-java-sdk</artifactId>
<version>0.3.0</version>
</dependency>Gradle (Kotlin DSL):
implementation("com.hawcx:hawcx-java-sdk:0.3.0")Step 2: Configure Environment Variables
Add the following to application.properties (Spring Boot), application.yml, or your environment:
# Base URL, from Admin Console → Project Settings (environment-specific)
hawcx.base-url=<Base URL from Admin Console>
# Config ID, from Admin Console → Project Settings (tenant routing key)
hawcx.config-id=<Config ID from Admin Console>
# Client ID, from Admin Console → Project Settings (expected `aud` on id_tokens)
hawcx.client-id=<Client ID from Admin Console>
# Optional (for delegation / MFA management):
hawcx.secret-key=hwx_sk_v1_...Where to find these values
Open the Hawcx Admin Console, go to Project Settings, and copy the Base URL, Config ID, and Client ID. All three are environment-specific.
configId vs clientId
These look similar but are distinct values. configId is the tenant
routing key sent as the x-config-id header on token exchange.
clientId is the value the tenant has configured as the aud claim on
issued id_tokens. The SDK passes each to the right place; you just need both
set correctly. See the Quickstart for details.
Step 3: Wire the OAuth Client as a Bean
Create a configuration class so the OAuth client is constructed once at startup
and reused across requests. HawcxOauthClient.fromIssuer(...) does the OIDC
discovery fetch (5s connect + 5s read), reads the token endpoint, JWKS URI,
and signing algorithm, and wires a thread-safe IDTokenValidator.
import com.hawcx.oauth.HawcxOauthClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HawcxConfiguration {
@Bean
public HawcxOauthClient hawcxOauthClient(
@Value("${hawcx.base-url}") String baseUrl,
@Value("${hawcx.config-id}") String configId,
@Value("${hawcx.client-id}") String clientId) {
// configId routes the token exchange (x-config-id header);
// clientId is the tenant-configured value the validator matches
// against the id_token's aud claim. Both come from the Admin Console.
return HawcxOauthClient.fromIssuer(baseUrl, configId, clientId);
}
}Fail-fast at startup: if the issuer is unreachable or the discovery
document is malformed, the bean creation throws HawcxOauthException and
Spring refuses to start. That's deliberate — an issuer you can't talk to
means you can't safely issue or verify tokens.
Step 4: Create an Exchange Endpoint
import com.hawcx.HawcxException;
import com.hawcx.oauth.HawcxOauthClient;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class AuthController {
private final HawcxOauthClient client;
private final UserService userService;
private final SessionService sessionService;
public AuthController(HawcxOauthClient client,
UserService userService,
SessionService sessionService) {
this.client = client;
this.userService = userService;
this.sessionService = sessionService;
}
public record ExchangeRequest(String authCode, String codeVerifier) {}
@PostMapping("/exchange")
public ResponseEntity<?> exchange(@RequestBody ExchangeRequest body) {
if (body.authCode() == null || body.codeVerifier() == null) {
return ResponseEntity.badRequest()
.body(java.util.Map.of("error", "Missing authCode or codeVerifier"));
}
try {
String idToken = client.exchangeCodeForIdToken(body.authCode(), body.codeVerifier());
IDTokenClaimsSet claims = client.verifyIdToken(idToken);
// Find or create user in your database
User user = userService.findOrCreate(
claims.getSubject().getValue(),
claims.getStringClaim("email")
);
// Create your application's session/JWT
String sessionToken = sessionService.generateToken(user);
return ResponseEntity.ok(java.util.Map.of(
"success", true,
"sessionToken", sessionToken,
"user", java.util.Map.of(
"id", user.getId(),
"email", user.getEmail()
)
));
} catch (HawcxException e) {
return ResponseEntity.status(401)
.body(java.util.Map.of("error", "Authentication failed"));
}
}
}Step 5: Integrate with Your User Management
Update your user service to handle Hawcx identities. claims.getSubject().getValue()
returns the stable Hawcx user identifier (sub claim); store it on your user records.
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public User findOrCreate(String hawcxId, String email) {
return repo.findByHawcxId(hawcxId)
.orElseGet(() -> repo.save(new User(hawcxId, email)));
}
}Optional: Backend-Driven MFA Management
Optional: advanced flows only
The delegation client is only required for backend-driven MFA setup/verify, suggested-MFA policy, device management, and step-up authentication. If your backend only needs the OAuth code exchange shown above, you can stop at Step 5.
If you need to manage MFA from your backend, add a DelegationClient bean and inject it where needed:
import com.hawcx.delegation.DelegationClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HawcxDelegationConfiguration {
@Bean
public DelegationClient hawcxDelegationClient(
@Value("${hawcx.base-url}") String baseUrl,
@Value("${hawcx.secret-key}") String secretKey,
@Value("${hawcx.config-id}") String configId) {
return DelegationClient.fromSecretKey(baseUrl, secretKey, configId);
}
}import com.google.gson.JsonObject;
import com.hawcx.delegation.DelegationClient;
import com.hawcx.delegation.MfaMethod;
import org.springframework.stereotype.Service;
@Service
public class MfaService {
private final DelegationClient client;
public MfaService(DelegationClient client) {
this.client = client;
}
public String startSmsMfa(String userId, String phoneNumber) {
JsonObject result = client.mfa.initiate(userId, MfaMethod.SMS, phoneNumber, null);
return result.get("session_id").getAsString();
}
public void verifyMfa(String userId, String sessionId, String otp) {
client.mfa.verify(userId, sessionId, otp);
}
public JsonObject listDevices(String userId) {
return client.devices.list(userId);
}
}Error Handling
Every SDK error extends com.hawcx.HawcxException. Catch the root if you only want a single handler:
import com.hawcx.HawcxException;
import com.hawcx.oauth.HawcxOauthException;
import com.hawcx.oauth.JwtVerificationException;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
try {
String idToken = client.exchangeCodeForIdToken(authCode, codeVerifier);
IDTokenClaimsSet claims = client.verifyIdToken(idToken);
} catch (JwtVerificationException e) {
// Signature, audience, issuer, expiration, or nbf validation failed.
// e.getCause() carries the underlying Nimbus exception (BadJWTException, etc.).
} catch (HawcxOauthException e) {
// Token endpoint returned non-2xx, response missing id_token, or — if the
// exception fires during application startup — discovery resolution failed.
// Inspect e.getError(): "discovery_failed", "invalid_discovery_document",
// "missing_id_token", "network_error", or a server-returned error code.
} catch (HawcxException e) {
// Catch-all for any SDK-originated failure
}For delegation calls, catch com.hawcx.delegation.DelegationException (or its subtypes DelegationCryptoException, DelegationRequestException, DelegationResponseException).