@multiplatform.one/wallet 7.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cjs/SiweKeycloakProvider.cjs +273 -0
- package/dist/cjs/SiweKeycloakProvider.native.js +309 -0
- package/dist/cjs/SiweKeycloakProvider.native.js.map +1 -0
- package/dist/cjs/base64.cjs +51 -0
- package/dist/cjs/base64.native.js +55 -0
- package/dist/cjs/base64.native.js.map +1 -0
- package/dist/cjs/base64.spec.cjs +57 -0
- package/dist/cjs/base64.spec.native.js +79 -0
- package/dist/cjs/base64.spec.native.js.map +1 -0
- package/dist/cjs/context.cjs +29 -0
- package/dist/cjs/context.native.js +32 -0
- package/dist/cjs/context.native.js.map +1 -0
- package/dist/cjs/hooks/context.cjs +29 -0
- package/dist/cjs/hooks/context.native.js +32 -0
- package/dist/cjs/hooks/context.native.js.map +1 -0
- package/dist/cjs/hooks/index.cjs +20 -0
- package/dist/cjs/hooks/index.native.js +23 -0
- package/dist/cjs/hooks/index.native.js.map +1 -0
- package/dist/cjs/hooks/useSiweKeycloak.cjs +61 -0
- package/dist/cjs/hooks/useSiweKeycloak.native.js +64 -0
- package/dist/cjs/hooks/useSiweKeycloak.native.js.map +1 -0
- package/dist/cjs/index.cjs +133 -0
- package/dist/cjs/index.native.js +189 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/render.cjs +78 -0
- package/dist/cjs/render.native.js +82 -0
- package/dist/cjs/render.native.js.map +1 -0
- package/dist/cjs/siwe.cjs +76 -0
- package/dist/cjs/siwe.native.js +94 -0
- package/dist/cjs/siwe.native.js.map +1 -0
- package/dist/cjs/types.cjs +18 -0
- package/dist/cjs/types.native.js +21 -0
- package/dist/cjs/types.native.js.map +1 -0
- package/dist/cjs/util.cjs +53 -0
- package/dist/cjs/util.native.js +56 -0
- package/dist/cjs/util.native.js.map +1 -0
- package/dist/cjs/util.spec.cjs +53 -0
- package/dist/cjs/util.spec.native.js +62 -0
- package/dist/cjs/util.spec.native.js.map +1 -0
- package/dist/esm/SiweKeycloakProvider.mjs +237 -0
- package/dist/esm/SiweKeycloakProvider.mjs.map +1 -0
- package/dist/esm/SiweKeycloakProvider.native.js +270 -0
- package/dist/esm/SiweKeycloakProvider.native.js.map +1 -0
- package/dist/esm/base64.mjs +26 -0
- package/dist/esm/base64.mjs.map +1 -0
- package/dist/esm/base64.native.js +27 -0
- package/dist/esm/base64.native.js.map +1 -0
- package/dist/esm/base64.spec.mjs +58 -0
- package/dist/esm/base64.spec.mjs.map +1 -0
- package/dist/esm/base64.spec.native.js +77 -0
- package/dist/esm/base64.spec.native.js.map +1 -0
- package/dist/esm/context.mjs +4 -0
- package/dist/esm/context.mjs.map +1 -0
- package/dist/esm/context.native.js +4 -0
- package/dist/esm/context.native.js.map +1 -0
- package/dist/esm/hooks/context.mjs +4 -0
- package/dist/esm/hooks/context.mjs.map +1 -0
- package/dist/esm/hooks/context.native.js +4 -0
- package/dist/esm/hooks/context.native.js.map +1 -0
- package/dist/esm/hooks/index.mjs +2 -0
- package/dist/esm/hooks/index.mjs.map +1 -0
- package/dist/esm/hooks/index.native.js +2 -0
- package/dist/esm/hooks/index.native.js.map +1 -0
- package/dist/esm/hooks/useSiweKeycloak.mjs +36 -0
- package/dist/esm/hooks/useSiweKeycloak.mjs.map +1 -0
- package/dist/esm/hooks/useSiweKeycloak.native.js +36 -0
- package/dist/esm/hooks/useSiweKeycloak.native.js.map +1 -0
- package/dist/esm/index.mjs +107 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +160 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/render.mjs +42 -0
- package/dist/esm/render.mjs.map +1 -0
- package/dist/esm/render.native.js +43 -0
- package/dist/esm/render.native.js.map +1 -0
- package/dist/esm/siwe.mjs +51 -0
- package/dist/esm/siwe.mjs.map +1 -0
- package/dist/esm/siwe.native.js +66 -0
- package/dist/esm/siwe.native.js.map +1 -0
- package/dist/esm/types.mjs +2 -0
- package/dist/esm/types.mjs.map +1 -0
- package/dist/esm/types.native.js +2 -0
- package/dist/esm/types.native.js.map +1 -0
- package/dist/esm/util.mjs +27 -0
- package/dist/esm/util.mjs.map +1 -0
- package/dist/esm/util.native.js +27 -0
- package/dist/esm/util.native.js.map +1 -0
- package/dist/esm/util.spec.mjs +54 -0
- package/dist/esm/util.spec.mjs.map +1 -0
- package/dist/esm/util.spec.native.js +60 -0
- package/dist/esm/util.spec.native.js.map +1 -0
- package/package.json +81 -0
- package/src/SiweKeycloakProvider.tsx +297 -0
- package/src/base64.spec.ts +65 -0
- package/src/base64.ts +26 -0
- package/src/context.ts +10 -0
- package/src/hooks/context.ts +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useSiweKeycloak.ts +23 -0
- package/src/index.ts +127 -0
- package/src/main/java/com/bitspur/keycloak/siwe/Errors.java +21 -0
- package/src/main/java/com/bitspur/keycloak/siwe/LiteAuthenticationService.java +167 -0
- package/src/main/java/com/bitspur/keycloak/siwe/SiweCredentials.java +36 -0
- package/src/main/java/com/bitspur/keycloak/siwe/SiweSession.java +36 -0
- package/src/main/java/com/bitspur/keycloak/siwe/SiweUtil.java +599 -0
- package/src/main/java/com/bitspur/keycloak/siwe/Token.java +85 -0
- package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticator.java +109 -0
- package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticatorFactory.java +93 -0
- package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticator.java +153 -0
- package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticatorFactory.java +93 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonce.java +95 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProvider.java +82 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProviderFactory.java +46 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletAddress.java +103 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProvider.java +43 -0
- package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProviderFactory.java +46 -0
- package/src/main/java/com/bitspur/keycloak/siwe/protocol/SiweWalletProtocolMapper.java +85 -0
- package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResource.java +210 -0
- package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProvider.java +34 -0
- package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProviderFactory.java +46 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuCredentials.java +52 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuMessage.java +362 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuParseException.java +23 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuUtil.java +547 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticator.java +112 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticatorFactory.java +96 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticator.java +169 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticatorFactory.java +96 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResource.java +244 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProvider.java +34 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProviderFactory.java +48 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsCredentials.java +52 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsMessage.java +362 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsParseException.java +23 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsUtil.java +564 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticator.java +112 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticatorFactory.java +96 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticator.java +169 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticatorFactory.java +96 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResource.java +244 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProvider.java +34 -0
- package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProviderFactory.java +48 -0
- package/src/main/resources/META-INF/keycloak-themes.json +8 -0
- package/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +6 -0
- package/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory +2 -0
- package/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +1 -0
- package/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +3 -0
- package/src/main/resources/META-INF/siwe-nonce-changelog.xml +29 -0
- package/src/main/resources/META-INF/siwe-wallet-changelog.xml +46 -0
- package/src/render.tsx +47 -0
- package/src/siwe.tsx +57 -0
- package/src/types.ts +6 -0
- package/src/util.spec.ts +72 -0
- package/src/util.ts +28 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticatorFactory.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Factory for the SIWS browser authenticator.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siws.authenticators.browser;
|
|
13
|
+
|
|
14
|
+
import java.util.List;
|
|
15
|
+
|
|
16
|
+
import org.keycloak.Config;
|
|
17
|
+
import org.keycloak.authentication.Authenticator;
|
|
18
|
+
import org.keycloak.authentication.AuthenticatorFactory;
|
|
19
|
+
import org.keycloak.models.AuthenticationExecutionModel;
|
|
20
|
+
import org.keycloak.models.KeycloakSession;
|
|
21
|
+
import org.keycloak.models.KeycloakSessionFactory;
|
|
22
|
+
import org.keycloak.provider.ProviderConfigProperty;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Factory for creating SIWS browser authenticator instances.
|
|
26
|
+
*/
|
|
27
|
+
public class SiwsBrowserAuthenticatorFactory implements AuthenticatorFactory {
|
|
28
|
+
public static final String ID = "siws-browser-authenticator";
|
|
29
|
+
public static final String DISPLAY_NAME = "SIWS Browser Authenticator";
|
|
30
|
+
|
|
31
|
+
private static final SiwsBrowserAuthenticator SINGLETON = new SiwsBrowserAuthenticator();
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
public String getId() {
|
|
35
|
+
return ID;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
public String getDisplayType() {
|
|
40
|
+
return DISPLAY_NAME;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Override
|
|
44
|
+
public String getHelpText() {
|
|
45
|
+
return "Authenticator for Sign-In With Solana (SIWS) in browser flows.";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public String getReferenceCategory() {
|
|
50
|
+
return "siws";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Override
|
|
54
|
+
public boolean isConfigurable() {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Override
|
|
59
|
+
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
|
60
|
+
return new AuthenticationExecutionModel.Requirement[] {
|
|
61
|
+
AuthenticationExecutionModel.Requirement.REQUIRED,
|
|
62
|
+
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
|
63
|
+
AuthenticationExecutionModel.Requirement.DISABLED
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public boolean isUserSetupAllowed() {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
public List<ProviderConfigProperty> getConfigProperties() {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
public Authenticator create(KeycloakSession session) {
|
|
79
|
+
return SINGLETON;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Override
|
|
83
|
+
public void init(Config.Scope config) {
|
|
84
|
+
// No initialization needed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Override
|
|
88
|
+
public void postInit(KeycloakSessionFactory factory) {
|
|
89
|
+
// No post-initialization needed
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Override
|
|
93
|
+
public void close() {
|
|
94
|
+
// Nothing to close
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticator.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Direct grant authenticator for Sign-In With Solana (SIWS).
|
|
10
|
+
* Used for API-based authentication (mobile apps, programmatic access).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
package com.bitspur.keycloak.siwe.siws.authenticators.directgrant;
|
|
14
|
+
|
|
15
|
+
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
|
|
16
|
+
|
|
17
|
+
import com.bitspur.keycloak.siwe.Errors.SiweKeycloakException;
|
|
18
|
+
import com.bitspur.keycloak.siwe.siws.SiwsCredentials;
|
|
19
|
+
import com.bitspur.keycloak.siwe.siws.SiwsUtil;
|
|
20
|
+
|
|
21
|
+
import jakarta.ws.rs.core.MediaType;
|
|
22
|
+
import jakarta.ws.rs.core.MultivaluedMap;
|
|
23
|
+
import jakarta.ws.rs.core.Response;
|
|
24
|
+
|
|
25
|
+
import org.jboss.logging.Logger;
|
|
26
|
+
import org.keycloak.authentication.AuthenticationFlowContext;
|
|
27
|
+
import org.keycloak.authentication.AuthenticationFlowError;
|
|
28
|
+
import org.keycloak.authentication.Authenticator;
|
|
29
|
+
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
|
30
|
+
import org.keycloak.events.Details;
|
|
31
|
+
import org.keycloak.events.Errors;
|
|
32
|
+
import org.keycloak.http.HttpRequest;
|
|
33
|
+
import org.keycloak.models.KeycloakSession;
|
|
34
|
+
import org.keycloak.models.RealmModel;
|
|
35
|
+
import org.keycloak.models.UserModel;
|
|
36
|
+
import org.keycloak.representations.idm.CredentialRepresentation;
|
|
37
|
+
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
|
38
|
+
import org.keycloak.services.managers.AuthenticationManager;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Direct grant SIWS authenticator for API/programmatic access.
|
|
42
|
+
* Accepts signature and message as form parameters.
|
|
43
|
+
*/
|
|
44
|
+
public class SiwsDirectGrantAuthenticator implements Authenticator {
|
|
45
|
+
private static final Logger logger = Logger.getLogger(SiwsDirectGrantAuthenticator.class);
|
|
46
|
+
|
|
47
|
+
@Override
|
|
48
|
+
public void authenticate(AuthenticationFlowContext context) {
|
|
49
|
+
String password = retrievePassword(context);
|
|
50
|
+
String username = retrieveUsername(context);
|
|
51
|
+
KeycloakSession session = context.getSession();
|
|
52
|
+
RealmModel realm = context.getRealm();
|
|
53
|
+
|
|
54
|
+
if (username == null) {
|
|
55
|
+
context.getEvent().error(Errors.USER_NOT_FOUND);
|
|
56
|
+
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED, "invalid_request",
|
|
57
|
+
"Missing parameter: username");
|
|
58
|
+
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
SiwsCredentials siwsCredentials = new SiwsCredentials(username, password);
|
|
64
|
+
String address = siwsCredentials.message.getAddress();
|
|
65
|
+
context.getEvent().detail(Details.USERNAME, address);
|
|
66
|
+
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, address);
|
|
67
|
+
|
|
68
|
+
UserModel user = SiwsUtil.findOrCreateUser(context, siwsCredentials, false);
|
|
69
|
+
if (user == null) {
|
|
70
|
+
context.getEvent().error(Errors.USER_NOT_FOUND);
|
|
71
|
+
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED, "invalid_grant",
|
|
72
|
+
"Invalid user credentials");
|
|
73
|
+
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check brute force protection
|
|
78
|
+
String bruteForceError = getDisabledByBruteForceEventError(context.getProtector(), session, realm, user);
|
|
79
|
+
if (bruteForceError != null) {
|
|
80
|
+
context.getEvent().user(user);
|
|
81
|
+
context.getEvent().error(bruteForceError);
|
|
82
|
+
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED, "invalid_grant",
|
|
83
|
+
"Invalid user credentials");
|
|
84
|
+
context.forceChallenge(challengeResponse);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if user is enabled
|
|
89
|
+
if (!user.isEnabled()) {
|
|
90
|
+
context.getEvent().user(user);
|
|
91
|
+
context.getEvent().error(Errors.USER_DISABLED);
|
|
92
|
+
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST, "invalid_grant",
|
|
93
|
+
"Account disabled");
|
|
94
|
+
context.forceChallenge(challengeResponse);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
context.setUser(user);
|
|
99
|
+
|
|
100
|
+
// Verify credentials
|
|
101
|
+
if (!SiwsUtil.verify(context.getSession(), context.getAuthenticatorConfig(), siwsCredentials)) {
|
|
102
|
+
context.getEvent().user(context.getUser());
|
|
103
|
+
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
|
104
|
+
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED, "invalid_grant",
|
|
105
|
+
"Invalid user credentials");
|
|
106
|
+
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
} catch (SiweKeycloakException e) {
|
|
110
|
+
logger.error(e);
|
|
111
|
+
context.getEvent().user(context.getUser());
|
|
112
|
+
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
|
113
|
+
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED, "invalid_grant",
|
|
114
|
+
e.getMessage());
|
|
115
|
+
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
context.success();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Override
|
|
122
|
+
public void action(AuthenticationFlowContext context) {
|
|
123
|
+
// Direct grant flows don't use action
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Override
|
|
127
|
+
public boolean requiresUser() {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Override
|
|
132
|
+
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Override
|
|
137
|
+
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
138
|
+
// No required actions
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Override
|
|
142
|
+
public void close() {
|
|
143
|
+
// Nothing to close
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected String retrieveUsername(AuthenticationFlowContext context) {
|
|
147
|
+
HttpRequest request = context.getHttpRequest();
|
|
148
|
+
MultivaluedMap<String, String> formData = request.getDecodedFormParameters();
|
|
149
|
+
String username = formData.getFirst(AuthenticationManager.FORM_USERNAME);
|
|
150
|
+
if (username == null) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return username.trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected String retrievePassword(AuthenticationFlowContext context) {
|
|
157
|
+
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
158
|
+
String password = formData.getFirst(CredentialRepresentation.PASSWORD);
|
|
159
|
+
if (password == null) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return password.trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected Response errorResponse(Response.Status status, String error, String errorDescription) {
|
|
166
|
+
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
|
|
167
|
+
return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticatorFactory.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Factory for the SIWS direct grant authenticator.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siws.authenticators.directgrant;
|
|
13
|
+
|
|
14
|
+
import java.util.List;
|
|
15
|
+
|
|
16
|
+
import org.keycloak.Config;
|
|
17
|
+
import org.keycloak.authentication.Authenticator;
|
|
18
|
+
import org.keycloak.authentication.AuthenticatorFactory;
|
|
19
|
+
import org.keycloak.models.AuthenticationExecutionModel;
|
|
20
|
+
import org.keycloak.models.KeycloakSession;
|
|
21
|
+
import org.keycloak.models.KeycloakSessionFactory;
|
|
22
|
+
import org.keycloak.provider.ProviderConfigProperty;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Factory for creating SIWS direct grant authenticator instances.
|
|
26
|
+
*/
|
|
27
|
+
public class SiwsDirectGrantAuthenticatorFactory implements AuthenticatorFactory {
|
|
28
|
+
public static final String ID = "siws-direct-grant-authenticator";
|
|
29
|
+
public static final String DISPLAY_NAME = "SIWS Direct Grant Authenticator";
|
|
30
|
+
|
|
31
|
+
private static final SiwsDirectGrantAuthenticator SINGLETON = new SiwsDirectGrantAuthenticator();
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
public String getId() {
|
|
35
|
+
return ID;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
public String getDisplayType() {
|
|
40
|
+
return DISPLAY_NAME;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Override
|
|
44
|
+
public String getHelpText() {
|
|
45
|
+
return "Authenticator for Sign-In With Solana (SIWS) in direct grant flows (API/mobile).";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public String getReferenceCategory() {
|
|
50
|
+
return "siws";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Override
|
|
54
|
+
public boolean isConfigurable() {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Override
|
|
59
|
+
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
|
60
|
+
return new AuthenticationExecutionModel.Requirement[] {
|
|
61
|
+
AuthenticationExecutionModel.Requirement.REQUIRED,
|
|
62
|
+
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
|
63
|
+
AuthenticationExecutionModel.Requirement.DISABLED
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public boolean isUserSetupAllowed() {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
public List<ProviderConfigProperty> getConfigProperties() {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
public Authenticator create(KeycloakSession session) {
|
|
79
|
+
return SINGLETON;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Override
|
|
83
|
+
public void init(Config.Scope config) {
|
|
84
|
+
// No initialization needed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Override
|
|
88
|
+
public void postInit(KeycloakSessionFactory factory) {
|
|
89
|
+
// No post-initialization needed
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Override
|
|
93
|
+
public void close() {
|
|
94
|
+
// Nothing to close
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResource.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* REST endpoints for Sign-In With Solana (SIWS).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siws.resource;
|
|
13
|
+
|
|
14
|
+
import com.bitspur.keycloak.siwe.Errors.SiweKeycloakException;
|
|
15
|
+
import com.bitspur.keycloak.siwe.LiteAuthenticationService;
|
|
16
|
+
import com.bitspur.keycloak.siwe.SiweSession;
|
|
17
|
+
import com.bitspur.keycloak.siwe.siws.SiwsCredentials;
|
|
18
|
+
import com.bitspur.keycloak.siwe.siws.SiwsUtil;
|
|
19
|
+
|
|
20
|
+
import jakarta.json.Json;
|
|
21
|
+
import jakarta.json.JsonObject;
|
|
22
|
+
import jakarta.json.JsonReader;
|
|
23
|
+
import jakarta.ws.rs.Consumes;
|
|
24
|
+
import jakarta.ws.rs.GET;
|
|
25
|
+
import jakarta.ws.rs.POST;
|
|
26
|
+
import jakarta.ws.rs.Path;
|
|
27
|
+
import jakarta.ws.rs.Produces;
|
|
28
|
+
import jakarta.ws.rs.core.MediaType;
|
|
29
|
+
import jakarta.ws.rs.core.Response;
|
|
30
|
+
|
|
31
|
+
import java.io.StringReader;
|
|
32
|
+
|
|
33
|
+
import org.jboss.logging.Logger;
|
|
34
|
+
import org.keycloak.models.KeycloakSession;
|
|
35
|
+
import org.keycloak.models.UserModel;
|
|
36
|
+
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* REST resource for SIWS operations.
|
|
40
|
+
* Provides endpoints for nonce generation, session management, and wallet registration.
|
|
41
|
+
*/
|
|
42
|
+
@Path("/siws")
|
|
43
|
+
public class SiwsResource {
|
|
44
|
+
private static final Logger logger = Logger.getLogger(SiwsResource.class);
|
|
45
|
+
|
|
46
|
+
private final KeycloakSession session;
|
|
47
|
+
private final LiteAuthenticationService authService;
|
|
48
|
+
|
|
49
|
+
public SiwsResource(KeycloakSession session) {
|
|
50
|
+
this.session = session;
|
|
51
|
+
this.authService = new LiteAuthenticationService(session);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GET /siws/nonce
|
|
56
|
+
* Generates a new nonce for SIWS authentication.
|
|
57
|
+
*
|
|
58
|
+
* @return The nonce as plain text, sets SIWS session cookie
|
|
59
|
+
*/
|
|
60
|
+
@GET
|
|
61
|
+
@Path("/nonce")
|
|
62
|
+
@Produces(MediaType.TEXT_PLAIN)
|
|
63
|
+
public Response getNonce() {
|
|
64
|
+
SiweSession siwsSession = SiwsUtil.generateSiwsSession(session);
|
|
65
|
+
String nonce = siwsSession.getNonce();
|
|
66
|
+
String sessionKey = siwsSession.getKey();
|
|
67
|
+
|
|
68
|
+
// Create secure HTTP-only cookie using Set-Cookie header
|
|
69
|
+
String cookieString = String.format("%s=%s; Path=/; Secure; HttpOnly; SameSite=None",
|
|
70
|
+
SiwsUtil.KEYCLOAK_SESSION_SIWS, sessionKey);
|
|
71
|
+
|
|
72
|
+
return Response.ok(nonce)
|
|
73
|
+
.header("Set-Cookie", cookieString)
|
|
74
|
+
.header("Access-Control-Allow-Origin", "*")
|
|
75
|
+
.header("Access-Control-Allow-Credentials", "true")
|
|
76
|
+
.build();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* GET /siws/session
|
|
81
|
+
* Returns the current user's session information.
|
|
82
|
+
*
|
|
83
|
+
* @return JSON with address and chainId
|
|
84
|
+
*/
|
|
85
|
+
@GET
|
|
86
|
+
@Path("/session")
|
|
87
|
+
@Produces(MediaType.APPLICATION_JSON)
|
|
88
|
+
public Response getSession() {
|
|
89
|
+
try {
|
|
90
|
+
UserModel user = getCurrentUser();
|
|
91
|
+
if (user == null) {
|
|
92
|
+
return Response.status(Response.Status.UNAUTHORIZED)
|
|
93
|
+
.entity("{\"error\":\"Not authenticated\"}")
|
|
94
|
+
.build();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get user's Solana wallet addresses
|
|
98
|
+
var wallets = SiwsUtil.getWalletAddresses(session, user);
|
|
99
|
+
String address = wallets.isEmpty() ? null : wallets.get(0).getAddress();
|
|
100
|
+
|
|
101
|
+
String jsonResponse = "{\"address\":\"" + (address != null ? address : "") +
|
|
102
|
+
"\",\"chainId\":\"mainnet-beta\"}";
|
|
103
|
+
|
|
104
|
+
return Response.ok(jsonResponse).build();
|
|
105
|
+
} catch (Exception e) {
|
|
106
|
+
logger.error("Error getting SIWS session", e);
|
|
107
|
+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
|
108
|
+
.entity("{\"error\":\"" + e.getMessage() + "\"}")
|
|
109
|
+
.build();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* POST /siws/register
|
|
115
|
+
* Registers a new Solana wallet address for the authenticated user.
|
|
116
|
+
*
|
|
117
|
+
* @param body JSON with message (base64) and signature (base58)
|
|
118
|
+
* @return JSON with registered status
|
|
119
|
+
*/
|
|
120
|
+
@POST
|
|
121
|
+
@Path("/register")
|
|
122
|
+
@Consumes(MediaType.APPLICATION_JSON)
|
|
123
|
+
@Produces(MediaType.APPLICATION_JSON)
|
|
124
|
+
public Response registerWallet(String body) {
|
|
125
|
+
try {
|
|
126
|
+
UserModel user = getCurrentUser();
|
|
127
|
+
if (user == null) {
|
|
128
|
+
return Response.status(Response.Status.UNAUTHORIZED)
|
|
129
|
+
.entity("{\"error\":\"Not authenticated\"}")
|
|
130
|
+
.build();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
JsonReader jsonReader = Json.createReader(new StringReader(body));
|
|
134
|
+
JsonObject jsonObject = jsonReader.readObject();
|
|
135
|
+
jsonReader.close();
|
|
136
|
+
|
|
137
|
+
String message = jsonObject.getString("message", null);
|
|
138
|
+
String signature = jsonObject.getString("signature", null);
|
|
139
|
+
|
|
140
|
+
if (message == null || signature == null) {
|
|
141
|
+
return Response.status(Response.Status.BAD_REQUEST)
|
|
142
|
+
.entity("{\"error\":\"Missing message or signature\"}")
|
|
143
|
+
.build();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
SiwsCredentials credentials = new SiwsCredentials(signature, message);
|
|
147
|
+
String address = credentials.message.getAddress();
|
|
148
|
+
|
|
149
|
+
// Verify the signature
|
|
150
|
+
if (!SiwsUtil.verifySiwsCredentials(session, SiwsUtil.getAuthenticatorConfig(session, false), credentials)) {
|
|
151
|
+
return Response.status(Response.Status.BAD_REQUEST)
|
|
152
|
+
.entity("{\"error\":\"Invalid signature\"}")
|
|
153
|
+
.build();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if wallet is already registered to another user
|
|
157
|
+
UserModel existingUser = SiwsUtil.findUserByWalletAddress(session, address);
|
|
158
|
+
if (existingUser != null && !existingUser.getId().equals(user.getId())) {
|
|
159
|
+
return Response.status(Response.Status.CONFLICT)
|
|
160
|
+
.entity("{\"error\":\"Wallet already registered to another user\"}")
|
|
161
|
+
.build();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Register the wallet
|
|
165
|
+
boolean registered = SiwsUtil.addWalletAddress(session, user, address);
|
|
166
|
+
|
|
167
|
+
return Response.ok("{\"registered\":" + registered + "}").build();
|
|
168
|
+
} catch (SiweKeycloakException e) {
|
|
169
|
+
logger.error("Error registering SIWS wallet", e);
|
|
170
|
+
return Response.status(Response.Status.BAD_REQUEST)
|
|
171
|
+
.entity("{\"error\":\"" + e.getMessage() + "\"}")
|
|
172
|
+
.build();
|
|
173
|
+
} catch (Exception e) {
|
|
174
|
+
logger.error("Unexpected error registering SIWS wallet", e);
|
|
175
|
+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
|
176
|
+
.entity("{\"error\":\"Internal server error\"}")
|
|
177
|
+
.build();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* POST /siws/unregister
|
|
183
|
+
* Unregisters a Solana wallet address from the authenticated user.
|
|
184
|
+
*
|
|
185
|
+
* @param body JSON with address
|
|
186
|
+
* @return JSON with unregistered status
|
|
187
|
+
*/
|
|
188
|
+
@POST
|
|
189
|
+
@Path("/unregister")
|
|
190
|
+
@Consumes(MediaType.APPLICATION_JSON)
|
|
191
|
+
@Produces(MediaType.APPLICATION_JSON)
|
|
192
|
+
public Response unregisterWallet(String body) {
|
|
193
|
+
try {
|
|
194
|
+
UserModel user = getCurrentUser();
|
|
195
|
+
if (user == null) {
|
|
196
|
+
return Response.status(Response.Status.UNAUTHORIZED)
|
|
197
|
+
.entity("{\"error\":\"Not authenticated\"}")
|
|
198
|
+
.build();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
JsonReader jsonReader = Json.createReader(new StringReader(body));
|
|
202
|
+
JsonObject jsonObject = jsonReader.readObject();
|
|
203
|
+
jsonReader.close();
|
|
204
|
+
|
|
205
|
+
String address = jsonObject.getString("address", null);
|
|
206
|
+
|
|
207
|
+
if (address == null) {
|
|
208
|
+
return Response.status(Response.Status.BAD_REQUEST)
|
|
209
|
+
.entity("{\"error\":\"Missing address\"}")
|
|
210
|
+
.build();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Verify the wallet belongs to the current user
|
|
214
|
+
var wallets = SiwsUtil.getWalletAddresses(session, user);
|
|
215
|
+
boolean ownsWallet = wallets.stream()
|
|
216
|
+
.anyMatch(w -> w.getAddress().equalsIgnoreCase(address.trim()));
|
|
217
|
+
|
|
218
|
+
if (!ownsWallet) {
|
|
219
|
+
return Response.status(Response.Status.NOT_FOUND)
|
|
220
|
+
.entity("{\"error\":\"Wallet not found for current user\"}")
|
|
221
|
+
.build();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
boolean unregistered = SiwsUtil.removeWalletAddress(session, address);
|
|
225
|
+
|
|
226
|
+
return Response.ok("{\"unregistered\":" + unregistered + "}").build();
|
|
227
|
+
} catch (Exception e) {
|
|
228
|
+
logger.error("Error unregistering SIWS wallet", e);
|
|
229
|
+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
|
230
|
+
.entity("{\"error\":\"" + e.getMessage() + "\"}")
|
|
231
|
+
.build();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private UserModel getCurrentUser() {
|
|
236
|
+
AuthResult authResult = authService.resolveAuthentication(
|
|
237
|
+
session.getContext().getRequestHeaders(),
|
|
238
|
+
session.getContext().getConnection());
|
|
239
|
+
if (authResult == null) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return authResult.getUser();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProvider.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.bitspur.keycloak.siwe.siws.resource;
|
|
11
|
+
|
|
12
|
+
import org.keycloak.models.KeycloakSession;
|
|
13
|
+
import org.keycloak.services.resource.RealmResourceProvider;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider for SIWS REST resources.
|
|
17
|
+
*/
|
|
18
|
+
public class SiwsResourceProvider implements RealmResourceProvider {
|
|
19
|
+
private final KeycloakSession session;
|
|
20
|
+
|
|
21
|
+
public SiwsResourceProvider(KeycloakSession session) {
|
|
22
|
+
this.session = session;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
public Object getResource() {
|
|
27
|
+
return new SiwsResource(session);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Override
|
|
31
|
+
public void close() {
|
|
32
|
+
// Nothing to close
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProviderFactory.java
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProviderFactory.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.bitspur.keycloak.siwe.siws.resource;
|
|
11
|
+
|
|
12
|
+
import org.keycloak.Config;
|
|
13
|
+
import org.keycloak.models.KeycloakSession;
|
|
14
|
+
import org.keycloak.models.KeycloakSessionFactory;
|
|
15
|
+
import org.keycloak.services.resource.RealmResourceProvider;
|
|
16
|
+
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Factory for creating SIWS resource providers.
|
|
20
|
+
*/
|
|
21
|
+
public class SiwsResourceProviderFactory implements RealmResourceProviderFactory {
|
|
22
|
+
public static final String ID = "siws";
|
|
23
|
+
|
|
24
|
+
@Override
|
|
25
|
+
public String getId() {
|
|
26
|
+
return ID;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public RealmResourceProvider create(KeycloakSession session) {
|
|
31
|
+
return new SiwsResourceProvider(session);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Override
|
|
35
|
+
public void init(Config.Scope config) {
|
|
36
|
+
// No initialization needed
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Override
|
|
40
|
+
public void postInit(KeycloakSessionFactory factory) {
|
|
41
|
+
// No post-initialization needed
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Override
|
|
45
|
+
public void close() {
|
|
46
|
+
// Nothing to close
|
|
47
|
+
}
|
|
48
|
+
}
|