This Java client connects with Privy.io, enabling simple user management and secure event handling. It also includes a Spring Boot starter for quick integration.
- Privy Client for Java
- Installation
- Client
- Configuration
- Usage
- Stream Users
- Stream Users by a Search Term
- Find a User by an ID
- Find a User by an Email Address
- Find a User by a Wallet Address
- Find a User by a Phone Number
- Find a User by a Twitter Username
- Find a User by a Twitter Subject
- Find a User by a Discord Username
- Find a User by a Github Username
- Find a User by a Custom Auth Id
- Set Custom Metadata for a User
- Delete a User by an ID
- Linked Accounts
- Get the Verification Key
- Verity an Auth Token
- Get a User from an ID Token
- Advanced Configuration
- Webhook
- Spring Boot Starter
<properties>
<privy.version>0.9.0</privy.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.caceresenzo.privy</groupId>
<artifactId>client</artifactId>
<version>${privy.version}</version>
</dependency>
</dependencies>PrivyClient client = PrivyClient.builder()
.applicationId("a0b1c2d3e4f5g6h7i8j9k0l1m")
.applicationSecret("a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5a6b7c8d9e0f1g2h3i4j5k6l7m8n9o0p1q2r3")
.build();Stream<User> users = client.findAllUsers();
/* or get a list via */
List<User> users = client.findAllUsers().toList();Stream<User> users = client.findAllUsers("john");
/* or get a list via */
List<User> users = client.findAllUsers("john").toList();Optional<User> user = client.findUserById("did:privy:a0b1c2d3e4f5g6h7i8j9k0l1m");
/* the "did:privy:" prefix is optional */
Optional<User> user = client.findUserById("a0b1c2d3e4f5g6h7i8j9k0l1m");Optional<User> user = client.findUserByEmail("john.doe@gmail.com");Optional<User> user = client.findUserByWallet("0xa0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9");Optional<User> user = client.findUserByPhone("+1 (234) 567-8912");Optional<User> user = client.findUserByTwitterUsername("johndoe");Optional<User> user = client.findUserByTwitterSubject("1234567890");Optional<User> user = client.findUserByDiscordUsername("johndoe#0");Optional<User> user = client.findUserByGithubUsername("johndoe");Optional<User> user = client.findUserByCustomAuthId("123456");CustomMetadata metadata = new CustomMetadata();
metadata.putString("planet", "Earth");
metadata.putNumber("age", 42);
metadata.putBoolean("powerful", true);
String userId = "a0b1c2d3e4f5g6h7i8j9k0l1m";
User updatedUser = client.setCustomMetadata(userId, metadata);boolean deleted = client.deleteUserById("0xa0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9");User user = client.findUserById("a0b1c2d3e4f5g6h7i8j9k0l1m").orElseThrow();
/* access it via known type */
Optional<LinkedAccount.Email> email = user.getEmail();
Optional<LinkedAccount.Google> google = user.getGoogle();
Optional<LinkedAccount.Github> github = user.getGithub();
/* access it via class */
Optional<LinkedAccount.Email> email = user.getAccount(LinkedAccount.Email.class);
Optional<LinkedAccount.Google> google = user.getAccount(LinkedAccount.Google.class);
Optional<LinkedAccount.Github> github = user.getAccount(LinkedAccount.Github.class);
/* iterate over accounts */
for (LinkedAccount account : user.getLinkedAccounts()) {
System.out.println(account);
// switch (account)
// see "Testing the linked account type"
}Testing the linked account type
switch (account) {
case LinkedAccount.Wallet wallet -> {
System.out.println("Wallet");
System.out.println(" with address: %s".formatted(wallet.getAddress()));
System.out.println(" with chain id: %s".formatted(wallet.getChainId()));
}
case LinkedAccount.Email email -> {
System.out.println("Email");
System.out.println(" with address: %s".formatted(email.getAddress()));
}
case LinkedAccount.Phone phone -> {
System.out.println("Phone");
System.out.println(" with number: %s".formatted(phone.getNumber()));
}
case LinkedAccount.Google google -> {
System.out.println("Google %s".formatted(google.getSubject()));
System.out.println(" with email: %s".formatted(google.getEmail()));
System.out.println(" with name: %s".formatted(google.getName()));
}
case LinkedAccount.Twitter twitter -> {
System.out.println("Twitter %s".formatted(twitter.getSubject()));
System.out.println(" with username: %s".formatted(twitter.getUsername()));
System.out.println(" with name: %s".formatted(twitter.getName()));
}
case LinkedAccount.Discord discord -> {
System.out.println("Discord %s".formatted(discord.getSubject()));
System.out.println(" with username: %s".formatted(discord.getUsername()));
System.out.println(" with email: %s".formatted(discord.getEmail()));
}
case LinkedAccount.Github github -> {
System.out.println("Github %s".formatted(github.getSubject()));
System.out.println(" with username: %s".formatted(github.getUsername()));
System.out.println(" with name: %s".formatted(github.getName()));
}
case LinkedAccount.LinkedIn linkedIn -> {
System.out.println("LinkedIn %s".formatted(linkedIn.getSubject()));
System.out.println(" with name: %s".formatted(linkedIn.getName()));
}
case LinkedAccount.Passkey passkey -> {
System.out.println("Passkey");
System.out.println(" with credentials id: %s".formatted(passkey.getCredentialId()));
}
case LinkedAccount.Other other -> {
System.out.println("Unknown %s".formatted(other.getType()));
System.out.println(" with properties: %s".formatted(other.getProperties()));
}
}import java.security.PublicKey;
PublicKey verificationKey = client.getVerificationKey();Note
The verification key is cached by default.
This behaviour can be disabled via the .cacheVerificationKey(false) method when building the client.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
String token = request.getCookie("privy-token");
Jws<Claims> jwt = client.verifyAuthToken(token);
Claims payload = jwt.getPayload();
System.out.println("User ID: %s".formatted(payload.getSubject()));Tip
We recommend keeping the verification key caching enabled (default behavior) if it is being used for authenticating requests.
String idToken = request.getCookie("privy-id-token");
User user = client.getUserFromIdToken(idToken);Tip
We recommend keeping the verification key caching enabled (default behavior) if it is being used for authenticating requests.
The client can be configured further to meet the demands of the application:
PrivyClient client = PrivyClient.builder()
/* change the api url */
.apiUrl("https://auth.privy.io")
/* mandatory credentials */
.applicationId("a0b1c2d3e4f5g6h7i8j9k0l1m")
.applicationSecret("a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5a6b7c8d9e0f1g2h3i4j5k6l7m8n9o0p1q2r3")
/* change the iterator page size */
.maxPageSize(100)
/* should the key obtained via `client.getVerificationKey()` be cached? */
.cacheVerificationKey(true)
/* configure the JWT parser, usually not recommended, but can be useful for testing purposes. */
.jwtParserCustomizer((builder) -> builder
.clockSkewSeconds(60)
.clock(new FixedClock(System.currentTimeMillis())) /* stop the world */
.unsecured() /* enable `alg: none` */
)
.build();Note
All values except those for applicationId, applicationSecret and jwtParserCustomizer are the default values.
PrivyWebhook webhook = PrivyWebhook.builder()
.signingKey("whsec_a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5")
.build();PrivyWebhook.Headers headers = new PrivyWebhook.Headers(
"msg_a0b1c2d3e4f5g6h7i8j9k0l1m2n",
"1234567890",
"v1,a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1"
);
String body = """
{
"message": "Hello, World!",
"type": "privy.test"
}
""";
Event event = webhook.verify(
headers,
body
);Testing the event type
switch (receivedEvent) {
case Event.Test event -> {
System.out.println("Testing: %s".formatted(event.getMessage()));
}
case Event.UserCreated event -> {
System.out.println("User Created: %s".formatted(event.getUser().getId()));
}
case Event.UserAuthenticated event -> {
System.out.println("User Authenticated: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
case Event.UserLinkedAccount event -> {
System.out.println("User Linked Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
case Event.UserUnlinkedAccount event -> {
System.out.println("User Unlinked Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
case Event.UserUpdatedAccount event -> {
System.out.println("User Updated Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
case Event.UserTransferredAccount event -> {
System.out.println("User Transferred Account: %s -> %s".formatted(event.getFromUser().getId(), event.getToUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
System.out.println(" and the old user was deleted? %s".formatted(event.isDeleted()));
}
case Event.UserWalletCreated event -> {
System.out.println("User Wallet Created: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWallet().getAddress()));
}
case Event.MultiFactorAuthenticationEnabled event -> {
System.out.println("Multi Factor Authentication Enabled: %s".formatted(event.getUserId()));
System.out.println(" with method: %s".formatted(event.getMethod()));
}
case Event.MultiFactorAuthenticationDisabled event -> {
System.out.println("Multi Factor Authentication Disabled: %s".formatted(event.getUserId()));
System.out.println(" with method: %s".formatted(event.getMethod()));
}
case Event.PrivateKeyExported event -> {
System.out.println("Private Key Exported: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
}
case Event.WalletRecoverySetup event -> {
System.out.println("Wallet Recovery Setup: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
System.out.println(" with method: %s".formatted(event.getMethod()));
}
case Event.WalletRecovered event -> {
System.out.println("Wallet Recovered: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
}
case Event.Other event -> {
System.out.println("Unknown event: %s".formatted(event.getType()));
System.out.println(" with properties: %s".formatted(event.getProperties()));
}
}There is a Spring Boot auto-configuration available.
<dependencies>
<dependency>
<groupId>dev.caceresenzo.privy</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${privy.version}</version>
</dependency>
</dependencies>Which is enabled when the Application ID is specified in the configuration:
privy:
application-id: a0b1c2d3e4f5g6h7i8j9k0l1m
application-secret: a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5a6b7c8d9e0f1g2h3i4j5k6l7m8n9o0p1q2r3Which is enabled when the Webhook Signing Key is specified in the configuration:
privy:
webhook-signing-key: whsec_a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5@RestController
@RequestMapping(path = "/privy/webhook", produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class ProvyRestControllerV1 {
private final PrivyWebhook privyWebhook;
@PostMapping
@ResponseStatus(HttpStatus.ACCEPTED)
public void webhook(
@RequestHeader(PrivyWebhook.Headers.ID_NAME) String id,
@RequestHeader(PrivyWebhook.Headers.TIMESTAMP_NAME) String timestamp,
@RequestHeader(PrivyWebhook.Headers.SIGNATURE_NAME) String signature,
@RequestBody String body
) {
PrivyWebhook.Headers headers = new PrivyWebhook.Headers(id, timestamp, signature);
Event event = privyWebhook.verify(headers, body);
System.out.println(event);
// switch (event)
// see "Testing the event type"
}
}In order for Privy authentication to work in the Spring OAuth 2.0 Resource Server, it must be configured as follows:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: ${PRIVY_JWKS_ENDPOINT}
jws-algorithms:
- ES256
issuer-uri: privy.io
# Optional, but increases security
audiences:
- ${PRIVY_APPLICATION_ID}@RestController
@RequestMapping(path = "/hello", produces = MediaType.APPLICATION_JSON_VALUE)
public class HelloRestController {
@GetMapping
@PreAuthorize("authenticated")
public String greet(
@AuthenticationPrincipal Jwt jwt
) {
String subject = jwt.getSubject();
return "Welcome %s!".formatted(subject);
}
}