@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.
Files changed (155) hide show
  1. package/README.md +8 -0
  2. package/dist/cjs/SiweKeycloakProvider.cjs +273 -0
  3. package/dist/cjs/SiweKeycloakProvider.native.js +309 -0
  4. package/dist/cjs/SiweKeycloakProvider.native.js.map +1 -0
  5. package/dist/cjs/base64.cjs +51 -0
  6. package/dist/cjs/base64.native.js +55 -0
  7. package/dist/cjs/base64.native.js.map +1 -0
  8. package/dist/cjs/base64.spec.cjs +57 -0
  9. package/dist/cjs/base64.spec.native.js +79 -0
  10. package/dist/cjs/base64.spec.native.js.map +1 -0
  11. package/dist/cjs/context.cjs +29 -0
  12. package/dist/cjs/context.native.js +32 -0
  13. package/dist/cjs/context.native.js.map +1 -0
  14. package/dist/cjs/hooks/context.cjs +29 -0
  15. package/dist/cjs/hooks/context.native.js +32 -0
  16. package/dist/cjs/hooks/context.native.js.map +1 -0
  17. package/dist/cjs/hooks/index.cjs +20 -0
  18. package/dist/cjs/hooks/index.native.js +23 -0
  19. package/dist/cjs/hooks/index.native.js.map +1 -0
  20. package/dist/cjs/hooks/useSiweKeycloak.cjs +61 -0
  21. package/dist/cjs/hooks/useSiweKeycloak.native.js +64 -0
  22. package/dist/cjs/hooks/useSiweKeycloak.native.js.map +1 -0
  23. package/dist/cjs/index.cjs +133 -0
  24. package/dist/cjs/index.native.js +189 -0
  25. package/dist/cjs/index.native.js.map +1 -0
  26. package/dist/cjs/render.cjs +78 -0
  27. package/dist/cjs/render.native.js +82 -0
  28. package/dist/cjs/render.native.js.map +1 -0
  29. package/dist/cjs/siwe.cjs +76 -0
  30. package/dist/cjs/siwe.native.js +94 -0
  31. package/dist/cjs/siwe.native.js.map +1 -0
  32. package/dist/cjs/types.cjs +18 -0
  33. package/dist/cjs/types.native.js +21 -0
  34. package/dist/cjs/types.native.js.map +1 -0
  35. package/dist/cjs/util.cjs +53 -0
  36. package/dist/cjs/util.native.js +56 -0
  37. package/dist/cjs/util.native.js.map +1 -0
  38. package/dist/cjs/util.spec.cjs +53 -0
  39. package/dist/cjs/util.spec.native.js +62 -0
  40. package/dist/cjs/util.spec.native.js.map +1 -0
  41. package/dist/esm/SiweKeycloakProvider.mjs +237 -0
  42. package/dist/esm/SiweKeycloakProvider.mjs.map +1 -0
  43. package/dist/esm/SiweKeycloakProvider.native.js +270 -0
  44. package/dist/esm/SiweKeycloakProvider.native.js.map +1 -0
  45. package/dist/esm/base64.mjs +26 -0
  46. package/dist/esm/base64.mjs.map +1 -0
  47. package/dist/esm/base64.native.js +27 -0
  48. package/dist/esm/base64.native.js.map +1 -0
  49. package/dist/esm/base64.spec.mjs +58 -0
  50. package/dist/esm/base64.spec.mjs.map +1 -0
  51. package/dist/esm/base64.spec.native.js +77 -0
  52. package/dist/esm/base64.spec.native.js.map +1 -0
  53. package/dist/esm/context.mjs +4 -0
  54. package/dist/esm/context.mjs.map +1 -0
  55. package/dist/esm/context.native.js +4 -0
  56. package/dist/esm/context.native.js.map +1 -0
  57. package/dist/esm/hooks/context.mjs +4 -0
  58. package/dist/esm/hooks/context.mjs.map +1 -0
  59. package/dist/esm/hooks/context.native.js +4 -0
  60. package/dist/esm/hooks/context.native.js.map +1 -0
  61. package/dist/esm/hooks/index.mjs +2 -0
  62. package/dist/esm/hooks/index.mjs.map +1 -0
  63. package/dist/esm/hooks/index.native.js +2 -0
  64. package/dist/esm/hooks/index.native.js.map +1 -0
  65. package/dist/esm/hooks/useSiweKeycloak.mjs +36 -0
  66. package/dist/esm/hooks/useSiweKeycloak.mjs.map +1 -0
  67. package/dist/esm/hooks/useSiweKeycloak.native.js +36 -0
  68. package/dist/esm/hooks/useSiweKeycloak.native.js.map +1 -0
  69. package/dist/esm/index.mjs +107 -0
  70. package/dist/esm/index.mjs.map +1 -0
  71. package/dist/esm/index.native.js +160 -0
  72. package/dist/esm/index.native.js.map +1 -0
  73. package/dist/esm/render.mjs +42 -0
  74. package/dist/esm/render.mjs.map +1 -0
  75. package/dist/esm/render.native.js +43 -0
  76. package/dist/esm/render.native.js.map +1 -0
  77. package/dist/esm/siwe.mjs +51 -0
  78. package/dist/esm/siwe.mjs.map +1 -0
  79. package/dist/esm/siwe.native.js +66 -0
  80. package/dist/esm/siwe.native.js.map +1 -0
  81. package/dist/esm/types.mjs +2 -0
  82. package/dist/esm/types.mjs.map +1 -0
  83. package/dist/esm/types.native.js +2 -0
  84. package/dist/esm/types.native.js.map +1 -0
  85. package/dist/esm/util.mjs +27 -0
  86. package/dist/esm/util.mjs.map +1 -0
  87. package/dist/esm/util.native.js +27 -0
  88. package/dist/esm/util.native.js.map +1 -0
  89. package/dist/esm/util.spec.mjs +54 -0
  90. package/dist/esm/util.spec.mjs.map +1 -0
  91. package/dist/esm/util.spec.native.js +60 -0
  92. package/dist/esm/util.spec.native.js.map +1 -0
  93. package/package.json +81 -0
  94. package/src/SiweKeycloakProvider.tsx +297 -0
  95. package/src/base64.spec.ts +65 -0
  96. package/src/base64.ts +26 -0
  97. package/src/context.ts +10 -0
  98. package/src/hooks/context.ts +10 -0
  99. package/src/hooks/index.ts +1 -0
  100. package/src/hooks/useSiweKeycloak.ts +23 -0
  101. package/src/index.ts +127 -0
  102. package/src/main/java/com/bitspur/keycloak/siwe/Errors.java +21 -0
  103. package/src/main/java/com/bitspur/keycloak/siwe/LiteAuthenticationService.java +167 -0
  104. package/src/main/java/com/bitspur/keycloak/siwe/SiweCredentials.java +36 -0
  105. package/src/main/java/com/bitspur/keycloak/siwe/SiweSession.java +36 -0
  106. package/src/main/java/com/bitspur/keycloak/siwe/SiweUtil.java +599 -0
  107. package/src/main/java/com/bitspur/keycloak/siwe/Token.java +85 -0
  108. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticator.java +109 -0
  109. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticatorFactory.java +93 -0
  110. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticator.java +153 -0
  111. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticatorFactory.java +93 -0
  112. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonce.java +95 -0
  113. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProvider.java +82 -0
  114. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProviderFactory.java +46 -0
  115. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletAddress.java +103 -0
  116. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProvider.java +43 -0
  117. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProviderFactory.java +46 -0
  118. package/src/main/java/com/bitspur/keycloak/siwe/protocol/SiweWalletProtocolMapper.java +85 -0
  119. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResource.java +210 -0
  120. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProvider.java +34 -0
  121. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProviderFactory.java +46 -0
  122. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuCredentials.java +52 -0
  123. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuMessage.java +362 -0
  124. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuParseException.java +23 -0
  125. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuUtil.java +547 -0
  126. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticator.java +112 -0
  127. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticatorFactory.java +96 -0
  128. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticator.java +169 -0
  129. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticatorFactory.java +96 -0
  130. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResource.java +244 -0
  131. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProvider.java +34 -0
  132. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProviderFactory.java +48 -0
  133. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsCredentials.java +52 -0
  134. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsMessage.java +362 -0
  135. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsParseException.java +23 -0
  136. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsUtil.java +564 -0
  137. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticator.java +112 -0
  138. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticatorFactory.java +96 -0
  139. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticator.java +169 -0
  140. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticatorFactory.java +96 -0
  141. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResource.java +244 -0
  142. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProvider.java +34 -0
  143. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProviderFactory.java +48 -0
  144. package/src/main/resources/META-INF/keycloak-themes.json +8 -0
  145. package/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +6 -0
  146. package/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory +2 -0
  147. package/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +1 -0
  148. package/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +3 -0
  149. package/src/main/resources/META-INF/siwe-nonce-changelog.xml +29 -0
  150. package/src/main/resources/META-INF/siwe-wallet-changelog.xml +46 -0
  151. package/src/render.tsx +47 -0
  152. package/src/siwe.tsx +57 -0
  153. package/src/types.ts +6 -0
  154. package/src/util.spec.ts +72 -0
  155. 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
+ }