Java Backend SDK Quickstart

Integrate Hawcx OAuth authentication in your Java backend

Hawcx OAuth Client SDK for Java

Add passwordless authentication to your Java backend. Exchange authorization codes for verified user claims and manage MFA.

Installation

The SDK is published to Maven Central as com.hawcx:hawcx-java-sdk. Java 21+ is required.

Maven

<dependency>
    <groupId>com.hawcx</groupId>
    <artifactId>hawcx-java-sdk</artifactId>
    <version>0.3.0</version>
</dependency>

Gradle (Kotlin DSL)

dependencies {
    implementation("com.hawcx:hawcx-java-sdk:0.3.0")
}

Gradle (Groovy)

dependencies {
    implementation 'com.hawcx:hawcx-java-sdk:0.3.0'
}

Quick Start

HawcxOauthClient.fromIssuer(...) discovers the OAuth endpoints once at startup from <issuer>/.well-known/openid-configuration, wires a standards-compliant IDTokenValidator (signature + iss + aud + exp + nbf), and gives you back a thread-safe client. No manual JWKS URL, no VerifyOptions builder.

import com.hawcx.oauth.HawcxOauthClient;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;

// Construct once and reuse — discovery hits the issuer's
// /.well-known/openid-configuration and caches the resolved endpoints + key set.
HawcxOauthClient client = HawcxOauthClient.fromIssuer(
        System.getenv("HAWCX_BASE_URL"),    // issuer URL
        System.getenv("HAWCX_CONFIG_ID"),   // → X-Config-Id header
        System.getenv("HAWCX_CLIENT_ID"));  // → expected aud on id_tokens

// Exchange the authorization code for a raw id_token
String idToken = client.exchangeCodeForIdToken(authCode, codeVerifier);

// Verify the id_token and read claims. Throws JwtVerificationException on
// any failure (wrong aud, expired, signature mismatch, malformed JWT, etc.).
IDTokenClaimsSet claims = client.verifyIdToken(idToken);
String userId = claims.getSubject().getValue();
String email = claims.getStringClaim("email");

configId and clientId are different values

Hawcx separates the tenant routing key from the audience claim:

  • configId — opaque tenant routing key, sent verbatim as the X-Config-Id header on token exchange so the OAuth server can look up the tenant.
  • clientId — the value the tenant has configured (in the Admin Console) as the aud claim on issued id_tokens. The validator checks the token's aud against this exact string.

Passing the same value for both fails verification with Unexpected JWT audience unless the tenant has configured them identically.

If your authorization request registered a redirect_uri (RFC 6749 §4.1.3 requires the token request to carry the same value), use the 3-arg overload:

String idToken = client.exchangeCodeForIdToken(authCode, codeVerifier,
                                                "https://app.example.com/auth/callback");

If your flow binds a nonce to the authorization request, pass the expected value to verifyIdToken so it's checked:

IDTokenClaimsSet claims = client.verifyIdToken(idToken, expectedNonce);

Need to inspect the resolved discovery doc (for debugging, e.g. "did we resolve the right token endpoint?"):

client.getDiscoveryMetadata().getTokenEndpointURI();
client.getDiscoveryMetadata().getJWKSetURI();
client.getDiscoveryMetadata().getIDTokenJWSAlgs();   // resolved signing algorithms

OAuth Code Exchange (legacy)

The legacy path is still supported for backward compatibility with v0.2.x. It manages JWKS and verification explicitly. Prefer the discovery path above for new code.

The most common flow: exchange authCode + codeVerifier for verified user claims.

import com.google.gson.JsonObject;
import com.hawcx.oauth.HawcxOauthClient;
import com.hawcx.oauth.HawcxOauthConfig;
import com.hawcx.oauth.JwtVerifier;
import com.hawcx.oauth.TokenRequest;

HawcxOauthConfig config = new HawcxOauthConfig(System.getenv("HAWCX_BASE_URL"));
HawcxOauthClient client = new HawcxOauthClient(config);

TokenRequest request = new TokenRequest(
    System.getenv("HAWCX_CONFIG_ID"),
    authCode,
    codeVerifier
);

JwtVerifier.VerifyOptions opts = JwtVerifier.VerifyOptions.builder()
    .jwksUrl(config.keysEndpoint())
    .audience(System.getenv("HAWCX_CONFIG_ID"))
    .issuer(System.getenv("HAWCX_BASE_URL"))
    .leewaySeconds(10)
    .build();

JsonObject claims = client.exchangeCodeForClaims(request, opts);
String userId = claims.get("sub").getAsString();
String email = claims.has("email") ? claims.get("email").getAsString() : null;

If you also need the raw id_token (for example, to set as a cookie), use exchangeCodeForTokenAndClaims which returns both:

HawcxOauthClient.TokenAndClaims result = client.exchangeCodeForTokenAndClaims(request, opts);
String idToken = result.tokenResponse().getIdToken();
JsonObject claims = result.claims();

Hawcx Delegation Client (MFA Setup)

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 above, you can skip this entirely.

Recommended for management auth: private_key_jwt

For management / step-up calls, prefer StepUpClient.withPrivateKeyJwt (Path A) — it reuses your OIDC private_key_jwt signing key (one key, RFC 7523) instead of the ECIES secret-key blob. The DelegationClient below is the legacy ECIES path (still supported).

Recommended — StepUpClient with private_key_jwt:

import com.hawcx.delegation.StepUpClient;
import com.hawcx.management.StartTokenResponse;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

// Reuses the same Ed25519 key you registered for OIDC login (one key, RFC 7523).
// Load it as a java.security.PrivateKey — example for a PKCS#8 PEM in an env var:
String pem = System.getenv("HAWCX_OIDC_PRIVATE_KEY_PEM")
        .replaceAll("-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\\s", "");
PrivateKey privateKey = KeyFactory.getInstance("Ed25519")
        .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem)));

StepUpClient stepUp = StepUpClient.withPrivateKeyJwt(
        System.getenv("HAWCX_BASE_URL"),
        System.getenv("HAWCX_CONFIG_ID"),
        privateKey,
        System.getenv("HAWCX_PRIVATE_KEY_KID"),  // key id from Admin Console
        System.getenv("HAWCX_CLIENT_ID")
);

// Begin a step-up flow (purposes: "change_mfa_method", "change_phone_number")
StartTokenResponse token = stepUp.startToken(
        "[email protected]", "change_mfa_method", "email_otp");

See Step-Up Client (Management API) for consumeReceipt, getMfaEnforcement, and full method details.

Legacy — ECIES DelegationClient (still supported); for setting up MFA for users programmatically:

import com.google.gson.JsonObject;
import com.hawcx.delegation.DelegationClient;
import com.hawcx.delegation.MfaMethod;

DelegationClient client = DelegationClient.fromSecretKey(
    System.getenv("HAWCX_BASE_URL"),
    System.getenv("HAWCX_SECRET_KEY"),    // hwx_sk_v1_...
    System.getenv("HAWCX_CONFIG_ID")
);

// Initiate MFA setup (Email, SMS, or TOTP)
JsonObject result = client.mfa.initiate(
    "[email protected]",
    MfaMethod.SMS,
    "+15551234567",
    null
);
String sessionId = result.get("session_id").getAsString();

// Verify OTP and complete MFA setup
client.mfa.verify("[email protected]", sessionId, "123456");

// Get user credentials
JsonObject creds = client.users.getCredentials("[email protected]");
String mfaMethod = creds.has("mfa_method") ? creds.get("mfa_method").getAsString() : null;

All delegation requests are automatically encrypted (ECIES X25519 + AES-GCM) and signed (Ed25519 or RSA-PSS); you only handle plaintext payloads.

Configuration

Environment Variables

For OAuth Code Exchange:

# Base URL, from Admin Console → Project Settings (environment-specific)
HAWCX_BASE_URL="<Base URL from Admin Console>"
# Config ID, from Admin Console → Project Settings
HAWCX_CONFIG_ID="<Config ID from Admin Console>"
# Client ID, from Admin Console → Project Settings (used as id_token `aud`)
HAWCX_CLIENT_ID="<Client ID from Admin Console>"

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. HAWCX_CLIENT_ID is only required by the discovery-mode API (fromIssuer); legacy callers that use JwtVerifier.VerifyOptions.builder().audience(...) can configure the audience there directly.

For Hawcx Delegation:

HAWCX_SECRET_KEY="hwx_sk_v1_..."
HAWCX_CONFIG_ID="<Config ID from Admin Console>"
HAWCX_BASE_URL="<Base URL from Admin Console>"

If you need to supply the underlying signing/encryption keys directly instead of the hwx_sk_v1_... blob, use DelegationClient.fromKeys(baseUrl, KeyMaterial keys, configId) with a KeyMaterial builder.

Integration Examples

All examples below use the recommended discovery API. They construct HawcxOauthClient once and reuse it across requests — the discovery fetch is bounded at 5s connect + 5s read so an unreachable issuer fails fast at startup rather than hanging.

Spring Boot

import com.hawcx.oauth.HawcxOauthClient;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Configuration
class HawcxConfiguration {
    @Bean
    HawcxOauthClient hawcxOauthClient(
            @Value("${hawcx.base-url}") String baseUrl,
            @Value("${hawcx.config-id}") String configId,
            @Value("${hawcx.client-id}") String clientId) {
        return HawcxOauthClient.fromIssuer(baseUrl, configId, clientId);
    }
}

@RestController
public class AuthController {

    private final HawcxOauthClient client;

    public AuthController(HawcxOauthClient client) {
        this.client = client;
    }

    public record ExchangeRequest(String authCode, String codeVerifier) {}

    @PostMapping("/exchange")
    public ResponseEntity<?> exchange(@RequestBody ExchangeRequest body) {
        try {
            String idToken = client.exchangeCodeForIdToken(body.authCode(), body.codeVerifier());
            IDTokenClaimsSet claims = client.verifyIdToken(idToken);
            return ResponseEntity.ok(java.util.Map.of(
                    "success", true,
                    "userId", claims.getSubject().getValue(),
                    "email", claims.getStringClaim("email")
            ));
        } catch (Exception e) {
            return ResponseEntity.status(401).body(java.util.Map.of("error", "Authentication failed"));
        }
    }
}

Quarkus

import com.hawcx.oauth.HawcxOauthClient;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
class HawcxClientProducer {
    @Inject @ConfigProperty(name = "hawcx.base-url")   String baseUrl;
    @Inject @ConfigProperty(name = "hawcx.config-id")  String configId;
    @Inject @ConfigProperty(name = "hawcx.client-id")  String clientId;

    @jakarta.enterprise.inject.Produces @ApplicationScoped
    HawcxOauthClient hawcxOauthClient() {
        return HawcxOauthClient.fromIssuer(baseUrl, configId, clientId);
    }
}

@Path("/exchange")
public class AuthResource {

    @Inject HawcxOauthClient client;

    public record ExchangeRequest(String authCode, String codeVerifier) {}

    @POST
    @Consumes("application/json")
    @Produces("application/json")
    public Response exchange(ExchangeRequest body) {
        try {
            String idToken = client.exchangeCodeForIdToken(body.authCode(), body.codeVerifier());
            IDTokenClaimsSet claims = client.verifyIdToken(idToken);
            return Response.ok(java.util.Map.of(
                    "success", true,
                    "userId", claims.getSubject().getValue()
            )).build();
        } catch (Exception e) {
            return Response.status(401).entity(java.util.Map.of("error", "Authentication failed")).build();
        }
    }
}

Plain Java (Javalin)

import com.google.gson.Gson;
import com.hawcx.oauth.HawcxOauthClient;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import io.javalin.Javalin;

public class App {

    record ExchangeRequest(String authCode, String codeVerifier) {}

    public static void main(String[] args) {
        HawcxOauthClient client = HawcxOauthClient.fromIssuer(
                System.getenv("HAWCX_BASE_URL"),
                System.getenv("HAWCX_CONFIG_ID"),
                System.getenv("HAWCX_CLIENT_ID"));

        Gson gson = new Gson();
        Javalin.create()
                .post("/exchange", ctx -> {
                    ExchangeRequest body = gson.fromJson(ctx.body(), ExchangeRequest.class);
                    try {
                        String idToken = client.exchangeCodeForIdToken(body.authCode(), body.codeVerifier());
                        IDTokenClaimsSet claims = client.verifyIdToken(idToken);
                        ctx.json(java.util.Map.of(
                                "success", true,
                                "userId", claims.getSubject().getValue()
                        ));
                    } catch (Exception e) {
                        ctx.status(401).json(java.util.Map.of("error", "Authentication failed"));
                    }
                })
                .start(8080);
    }
}

PKCE and redirect_uri

codeVerifier is required for the exchange. Store it on the client and send it alongside authCode when your backend exchanges the code.

If your original authorization request included a redirect_uri, RFC 6749 §4.1.3 requires the token request to carry the same value. Both APIs support this:

Discovery API (recommended) — 3-arg overload of exchangeCodeForIdToken:

String idToken = client.exchangeCodeForIdToken(authCode, codeVerifier,
                                                "https://app.example.com/auth/callback");

Legacy API — 4-arg TokenRequest constructor:

TokenRequest req = new TokenRequest(configId, authCode, codeVerifier,
                                     "https://app.example.com/auth/callback");

Confidential clients (private_key_jwt)

Optional — most apps stay on PKCE. For a backend that should authenticate with a key it holds, switch the project to Enhanced mode in the Admin Console, then attach the Ed25519 private JWK with withClientAssertion (discovery mode):

HawcxOauthClient client = HawcxOauthClient
    .fromIssuer(baseUrl, configId, clientId)
    .withClientAssertion(ClientAssertionSigner.ed25519FromJwk(privateKeyJwk));

String idToken = client.exchangeCodeForIdToken(authCode, codeVerifier);

See Confidential Clients (private_key_jwt) for the Admin Console setup and a no-SDK (stock JWT library) example.

Next Steps

  • View the complete API reference for advanced features and detailed method signatures
  • Set up MFA management for your users
  • Join our developer community on Slack for support and updates