@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/siu/authenticators/browser/SiuBrowserAuthenticatorFactory.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 SIU browser authenticator.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siu.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 SIU browser authenticator instances.
|
|
26
|
+
*/
|
|
27
|
+
public class SiuBrowserAuthenticatorFactory implements AuthenticatorFactory {
|
|
28
|
+
public static final String ID = "siu-browser-authenticator";
|
|
29
|
+
public static final String DISPLAY_NAME = "SIU Browser Authenticator";
|
|
30
|
+
|
|
31
|
+
private static final SiuBrowserAuthenticator SINGLETON = new SiuBrowserAuthenticator();
|
|
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 Sui (SIU) in browser flows.";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public String getReferenceCategory() {
|
|
50
|
+
return "siu";
|
|
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/siu/authenticators/directgrant/SiuDirectGrantAuthenticator.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 Sui (SIU).
|
|
10
|
+
* Used for API-based authentication (mobile apps, programmatic access).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
package com.bitspur.keycloak.siwe.siu.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.siu.SiuCredentials;
|
|
19
|
+
import com.bitspur.keycloak.siwe.siu.SiuUtil;
|
|
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 SIU authenticator for API/programmatic access.
|
|
42
|
+
* Accepts signature and message as form parameters.
|
|
43
|
+
*/
|
|
44
|
+
public class SiuDirectGrantAuthenticator implements Authenticator {
|
|
45
|
+
private static final Logger logger = Logger.getLogger(SiuDirectGrantAuthenticator.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
|
+
SiuCredentials siuCredentials = new SiuCredentials(username, password);
|
|
64
|
+
String address = siuCredentials.message.getAddress();
|
|
65
|
+
context.getEvent().detail(Details.USERNAME, address);
|
|
66
|
+
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, address);
|
|
67
|
+
|
|
68
|
+
UserModel user = SiuUtil.findOrCreateUser(context, siuCredentials, 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 (!SiuUtil.verify(context.getSession(), context.getAuthenticatorConfig(), siuCredentials)) {
|
|
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/siu/authenticators/directgrant/SiuDirectGrantAuthenticatorFactory.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 SIU direct grant authenticator.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siu.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 SIU direct grant authenticator instances.
|
|
26
|
+
*/
|
|
27
|
+
public class SiuDirectGrantAuthenticatorFactory implements AuthenticatorFactory {
|
|
28
|
+
public static final String ID = "siu-direct-grant-authenticator";
|
|
29
|
+
public static final String DISPLAY_NAME = "SIU Direct Grant Authenticator";
|
|
30
|
+
|
|
31
|
+
private static final SiuDirectGrantAuthenticator SINGLETON = new SiuDirectGrantAuthenticator();
|
|
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 Sui (SIU) in direct grant flows.";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public String getReferenceCategory() {
|
|
50
|
+
return "siu";
|
|
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/siu/resource/SiuResource.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 Sui (SIU).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siu.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.siu.SiuCredentials;
|
|
18
|
+
import com.bitspur.keycloak.siwe.siu.SiuUtil;
|
|
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 SIU operations.
|
|
40
|
+
* Provides endpoints for nonce generation, session management, and wallet registration.
|
|
41
|
+
*/
|
|
42
|
+
@Path("/siu")
|
|
43
|
+
public class SiuResource {
|
|
44
|
+
private static final Logger logger = Logger.getLogger(SiuResource.class);
|
|
45
|
+
|
|
46
|
+
private final KeycloakSession session;
|
|
47
|
+
private final LiteAuthenticationService authService;
|
|
48
|
+
|
|
49
|
+
public SiuResource(KeycloakSession session) {
|
|
50
|
+
this.session = session;
|
|
51
|
+
this.authService = new LiteAuthenticationService(session);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GET /siu/nonce
|
|
56
|
+
* Generates a new nonce for SIU authentication.
|
|
57
|
+
*
|
|
58
|
+
* @return The nonce as plain text, sets SIU session cookie
|
|
59
|
+
*/
|
|
60
|
+
@GET
|
|
61
|
+
@Path("/nonce")
|
|
62
|
+
@Produces(MediaType.TEXT_PLAIN)
|
|
63
|
+
public Response getNonce() {
|
|
64
|
+
SiweSession siuSession = SiuUtil.generateSiuSession(session);
|
|
65
|
+
String nonce = siuSession.getNonce();
|
|
66
|
+
String sessionKey = siuSession.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
|
+
SiuUtil.KEYCLOAK_SESSION_SIU, 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 /siu/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 Sui wallet addresses
|
|
98
|
+
var wallets = SiuUtil.getWalletAddresses(session, user);
|
|
99
|
+
String address = wallets.isEmpty() ? null : wallets.get(0).getAddress();
|
|
100
|
+
|
|
101
|
+
String jsonResponse = "{\"address\":\"" + (address != null ? address : "") +
|
|
102
|
+
"\",\"chainId\":\"sui:mainnet\"}";
|
|
103
|
+
|
|
104
|
+
return Response.ok(jsonResponse).build();
|
|
105
|
+
} catch (Exception e) {
|
|
106
|
+
logger.error("Error getting SIU session", e);
|
|
107
|
+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
|
108
|
+
.entity("{\"error\":\"" + e.getMessage() + "\"}")
|
|
109
|
+
.build();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* POST /siu/register
|
|
115
|
+
* Registers a new Sui wallet address for the authenticated user.
|
|
116
|
+
*
|
|
117
|
+
* @param body JSON with message (base64) and signature (base64)
|
|
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
|
+
SiuCredentials credentials = new SiuCredentials(signature, message);
|
|
147
|
+
String address = credentials.message.getAddress();
|
|
148
|
+
|
|
149
|
+
// Verify the signature
|
|
150
|
+
if (!SiuUtil.verifySiuCredentials(session, SiuUtil.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 = SiuUtil.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 = SiuUtil.addWalletAddress(session, user, address);
|
|
166
|
+
|
|
167
|
+
return Response.ok("{\"registered\":" + registered + "}").build();
|
|
168
|
+
} catch (SiweKeycloakException e) {
|
|
169
|
+
logger.error("Error registering SIU 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 SIU 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 /siu/unregister
|
|
183
|
+
* Unregisters a Sui 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 = SiuUtil.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 = SiuUtil.removeWalletAddress(session, address);
|
|
225
|
+
|
|
226
|
+
return Response.ok("{\"unregistered\":" + unregistered + "}").build();
|
|
227
|
+
} catch (Exception e) {
|
|
228
|
+
logger.error("Error unregistering SIU 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/siu/resource/SiuResourceProvider.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.siu.resource;
|
|
11
|
+
|
|
12
|
+
import org.keycloak.models.KeycloakSession;
|
|
13
|
+
import org.keycloak.services.resource.RealmResourceProvider;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider for SIU REST resources.
|
|
17
|
+
*/
|
|
18
|
+
public class SiuResourceProvider implements RealmResourceProvider {
|
|
19
|
+
private final KeycloakSession session;
|
|
20
|
+
|
|
21
|
+
public SiuResourceProvider(KeycloakSession session) {
|
|
22
|
+
this.session = session;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
public Object getResource() {
|
|
27
|
+
return new SiuResource(session);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Override
|
|
31
|
+
public void close() {
|
|
32
|
+
// Nothing to close
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProviderFactory.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.siu.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 SIU resource providers.
|
|
20
|
+
*/
|
|
21
|
+
public class SiuResourceProviderFactory implements RealmResourceProviderFactory {
|
|
22
|
+
public static final String ID = "siu";
|
|
23
|
+
|
|
24
|
+
@Override
|
|
25
|
+
public String getId() {
|
|
26
|
+
return ID;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public RealmResourceProvider create(KeycloakSession session) {
|
|
31
|
+
return new SiuResourceProvider(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
|
+
}
|