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.10.1</privy.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.caceresenzo.privy</groupId>
<artifactId>client</artifactId>
<version>${privy.version}</version>
</dependency>
</dependencies>PrivyClientclient = 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");CustomMetadatametadata = newCustomMetadata();
metadata.putString("planet", "Earth");
metadata.putNumber("age", 42);
metadata.putBoolean("powerful", true);
StringuserId = "a0b1c2d3e4f5g6h7i8j9k0l1m";
UserupdatedUser = client.setCustomMetadata(userId, metadata);booleandeleted = client.deleteUserById("0xa0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9");Useruser = 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 (LinkedAccountaccount : user.getLinkedAccounts()) {
System.out.println(account);
// switch (account)// see "Testing the linked account type"
}Testing the linked account type
switch (account) {
caseLinkedAccount.Walletwallet -> {
System.out.println("Wallet");
System.out.println(" with address: %s".formatted(wallet.getAddress()));
System.out.println(" with chain id: %s".formatted(wallet.getChainId()));
}
caseLinkedAccount.Emailemail -> {
System.out.println("Email");
System.out.println(" with address: %s".formatted(email.getAddress()));
}
caseLinkedAccount.Phonephone -> {
System.out.println("Phone");
System.out.println(" with number: %s".formatted(phone.getNumber()));
}
caseLinkedAccount.Googlegoogle -> {
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()));
}
caseLinkedAccount.Twittertwitter -> {
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()));
}
caseLinkedAccount.Discorddiscord -> {
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()));
}
caseLinkedAccount.Githubgithub -> {
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()));
}
caseLinkedAccount.LinkedInlinkedIn -> {
System.out.println("LinkedIn %s".formatted(linkedIn.getSubject()));
System.out.println(" with name: %s".formatted(linkedIn.getName()));
}
caseLinkedAccount.Passkeypasskey -> {
System.out.println("Passkey");
System.out.println(" with credentials id: %s".formatted(passkey.getCredentialId()));
}
caseLinkedAccount.Otherother -> {
System.out.println("Unknown %s".formatted(other.getType()));
System.out.println(" with properties: %s".formatted(other.getProperties()));
}
}importjava.security.PublicKey;
PublicKeyverificationKey = client.getVerificationKey();Note
The verification key is cached by default.
This behaviour can be disabled via the .cacheVerificationKey(false) method when building the client.
importio.jsonwebtoken.Claims;
importio.jsonwebtoken.Jws;
Stringtoken = request.getCookie("privy-token");
Jws<Claims> jwt = client.verifyAuthToken(token);
Claimspayload = 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.
StringidToken = request.getCookie("privy-id-token");
Useruser = 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:
PrivyClientclient = 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(newFixedClock(System.currentTimeMillis())) /* stop the world */
.unsecured() /* enable `alg: none` */
)
.build();Note
All values except those for applicationId, applicationSecret and jwtParserCustomizer are the default values.
PrivyWebhookwebhook = PrivyWebhook.builder()
.signingKey("whsec_a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5")
.build();PrivyWebhook.Headersheaders = newPrivyWebhook.Headers(
"msg_a0b1c2d3e4f5g6h7i8j9k0l1m2n",
"1234567890",
"v1,a0b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1"
);
Stringbody = """ { "message": "Hello, World!", "type": "privy.test" } """;
Eventevent = webhook.verify(
headers,
body
);Testing the event type
switch (receivedEvent) {
caseEvent.Testevent -> {
System.out.println("Testing: %s".formatted(event.getMessage()));
}
caseEvent.UserCreatedevent -> {
System.out.println("User Created: %s".formatted(event.getUser().getId()));
}
caseEvent.UserAuthenticatedevent -> {
System.out.println("User Authenticated: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
caseEvent.UserLinkedAccountevent -> {
System.out.println("User Linked Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
caseEvent.UserUnlinkedAccountevent -> {
System.out.println("User Unlinked Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
caseEvent.UserUpdatedAccountevent -> {
System.out.println("User Updated Account: %s".formatted(event.getUser().getId()));
System.out.println(" with account: %s".formatted(event.getAccount()));
}
caseEvent.UserTransferredAccountevent -> {
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()));
}
caseEvent.UserWalletCreatedevent -> {
System.out.println("User Wallet Created: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWallet().getAddress()));
}
caseEvent.MultiFactorAuthenticationEnabledevent -> {
System.out.println("Multi Factor Authentication Enabled: %s".formatted(event.getUserId()));
System.out.println(" with method: %s".formatted(event.getMethod()));
}
caseEvent.MultiFactorAuthenticationDisabledevent -> {
System.out.println("Multi Factor Authentication Disabled: %s".formatted(event.getUserId()));
System.out.println(" with method: %s".formatted(event.getMethod()));
}
caseEvent.WalletArchivedevent -> {
System.out.println("Wallet Archived: %s".formatted(event.getWalletAddress()));
}
caseEvent.WalletRestoredevent -> {
System.out.println("Wallet Restored: %s".formatted(event.getWalletAddress()));
}
caseEvent.FundsDepositedevent -> {
System.out.println("Funds Deposited: %s".formatted(event.getAsset()));
System.out.println(" from: %s".formatted(event.getSender()));
System.out.println(" to: %s".formatted(event.getRecipient()));
System.out.println(" for a total amount of: %s".formatted(event.getAmount()));
}
caseEvent.FundsWithdrawnevent -> {
System.out.println("Funds Withdrawn: %s".formatted(event.getAsset()));
System.out.println(" from: %s".formatted(event.getSender()));
System.out.println(" to: %s".formatted(event.getRecipient()));
System.out.println(" for a total amount of: %s".formatted(event.getAmount()));
}
caseEvent.PrivateKeyExportedevent -> {
System.out.println("Private Key Exported: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
}
caseEvent.SeedPhraseExportedevent -> {
System.out.println("Seed Phrase Exported: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
}
caseEvent.WalletRecoverySetupevent -> {
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()));
}
caseEvent.WalletRecoveredevent -> {
System.out.println("Wallet Recovered: %s".formatted(event.getUserId()));
System.out.println(" with wallet address: %s".formatted(event.getWalletAddress()));
}
caseEvent.Otherevent -> {
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: a0b1c2d3e4f5g6h7i8j9k0l1mapplication-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)
@RequiredArgsConstructorpublicclassProvyRestControllerV1 {
privatefinalPrivyWebhookprivyWebhook;
@PostMapping@ResponseStatus(HttpStatus.ACCEPTED)
publicvoidwebhook(
@RequestHeader(PrivyWebhook.Headers.ID_NAME) Stringid,
@RequestHeader(PrivyWebhook.Headers.TIMESTAMP_NAME) Stringtimestamp,
@RequestHeader(PrivyWebhook.Headers.SIGNATURE_NAME) Stringsignature,
@RequestBodyStringbody
) {
PrivyWebhook.Headersheaders = newPrivyWebhook.Headers(id, timestamp, signature);
Eventevent = 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:
- ES256issuer-uri: privy.io# Optional, but increases securityaudiences:
- ${PRIVY_APPLICATION_ID}@RestController@RequestMapping(path = "/hello", produces = MediaType.APPLICATION_JSON_VALUE)
publicclassHelloRestController {
@GetMapping@PreAuthorize("authenticated")
publicStringgreet(
@AuthenticationPrincipalJwtjwt
) {
Stringsubject = jwt.getSubject();
return"Welcome %s!".formatted(subject);
}
}