@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,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/SiwsUtil.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Utility functions for Sign-In With Solana (SIWS) authentication.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siws;
|
|
13
|
+
|
|
14
|
+
import com.bitspur.keycloak.siwe.Errors.SiweKeycloakException;
|
|
15
|
+
import com.bitspur.keycloak.siwe.SiweSession;
|
|
16
|
+
import com.bitspur.keycloak.siwe.SiweUtil;
|
|
17
|
+
import com.bitspur.keycloak.siwe.jpa.nonce.SiweNonce;
|
|
18
|
+
import com.bitspur.keycloak.siwe.jpa.wallet.SiweWalletAddress;
|
|
19
|
+
import com.bitspur.keycloak.siwe.siws.authenticators.browser.SiwsBrowserAuthenticatorFactory;
|
|
20
|
+
import com.bitspur.keycloak.siwe.siws.authenticators.directgrant.SiwsDirectGrantAuthenticatorFactory;
|
|
21
|
+
import com.iwebpp.crypto.TweetNaclFast;
|
|
22
|
+
|
|
23
|
+
import java.nio.charset.StandardCharsets;
|
|
24
|
+
import java.security.SecureRandom;
|
|
25
|
+
import java.util.ArrayList;
|
|
26
|
+
import java.util.Base64;
|
|
27
|
+
import java.util.HashMap;
|
|
28
|
+
import java.util.List;
|
|
29
|
+
import java.util.Map;
|
|
30
|
+
|
|
31
|
+
import jakarta.persistence.EntityManager;
|
|
32
|
+
import jakarta.persistence.NoResultException;
|
|
33
|
+
import jakarta.persistence.TypedQuery;
|
|
34
|
+
import jakarta.ws.rs.core.Cookie;
|
|
35
|
+
import jakarta.ws.rs.core.MultivaluedHashMap;
|
|
36
|
+
import jakarta.ws.rs.core.MultivaluedMap;
|
|
37
|
+
|
|
38
|
+
import org.jboss.logging.Logger;
|
|
39
|
+
import org.keycloak.authentication.AuthenticationFlowContext;
|
|
40
|
+
import org.keycloak.authentication.AuthenticationFlowError;
|
|
41
|
+
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
|
42
|
+
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
|
43
|
+
import org.keycloak.events.Details;
|
|
44
|
+
import org.keycloak.events.Errors;
|
|
45
|
+
import org.keycloak.forms.login.LoginFormsProvider;
|
|
46
|
+
import org.keycloak.http.HttpRequest;
|
|
47
|
+
import org.keycloak.models.AuthenticationExecutionModel;
|
|
48
|
+
import org.keycloak.models.AuthenticationFlowModel;
|
|
49
|
+
import org.keycloak.models.AuthenticatorConfigModel;
|
|
50
|
+
import org.keycloak.models.KeycloakSession;
|
|
51
|
+
import org.keycloak.models.ModelDuplicateException;
|
|
52
|
+
import org.keycloak.models.RealmModel;
|
|
53
|
+
import org.keycloak.models.UserModel;
|
|
54
|
+
import org.keycloak.services.ServicesLogger;
|
|
55
|
+
import org.keycloak.services.messages.Messages;
|
|
56
|
+
import org.keycloak.userprofile.UserProfile;
|
|
57
|
+
import org.keycloak.userprofile.UserProfileContext;
|
|
58
|
+
import org.keycloak.userprofile.UserProfileProvider;
|
|
59
|
+
import org.keycloak.models.utils.KeycloakModelUtils;
|
|
60
|
+
|
|
61
|
+
public class SiwsUtil {
|
|
62
|
+
private static final Logger logger = Logger.getLogger(SiwsUtil.class);
|
|
63
|
+
private static final SecureRandom random = new SecureRandom();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Address type for Solana wallets.
|
|
67
|
+
*/
|
|
68
|
+
public static final String SOLANA_ADDRESS_TYPE = "solana";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cookie name for SIWS session.
|
|
72
|
+
*/
|
|
73
|
+
public static final String KEYCLOAK_SESSION_SIWS = "KEYCLOAK_SESSION_SIWS";
|
|
74
|
+
|
|
75
|
+
// Base58 alphabet used by Solana
|
|
76
|
+
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
77
|
+
private static final int[] BASE58_INDEXES = new int[128];
|
|
78
|
+
|
|
79
|
+
static {
|
|
80
|
+
for (int i = 0; i < BASE58_INDEXES.length; i++) {
|
|
81
|
+
BASE58_INDEXES[i] = -1;
|
|
82
|
+
}
|
|
83
|
+
for (int i = 0; i < BASE58_ALPHABET.length(); i++) {
|
|
84
|
+
BASE58_INDEXES[BASE58_ALPHABET.charAt(i)] = i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decodes a Base58 encoded string to bytes.
|
|
90
|
+
*
|
|
91
|
+
* @param input Base58 encoded string
|
|
92
|
+
* @return Decoded bytes
|
|
93
|
+
* @throws SiweKeycloakException if decoding fails
|
|
94
|
+
*/
|
|
95
|
+
public static byte[] decodeBase58(String input) throws SiweKeycloakException {
|
|
96
|
+
if (input == null || input.isEmpty()) {
|
|
97
|
+
throw new SiweKeycloakException("Cannot decode null or empty Base58 string");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Count leading zeros
|
|
101
|
+
int zeros = 0;
|
|
102
|
+
for (int i = 0; i < input.length() && input.charAt(i) == '1'; i++) {
|
|
103
|
+
zeros++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Allocate enough space for the decoded output
|
|
107
|
+
byte[] b256 = new byte[input.length()];
|
|
108
|
+
int outputLen = 0;
|
|
109
|
+
|
|
110
|
+
for (int i = zeros; i < input.length(); i++) {
|
|
111
|
+
char c = input.charAt(i);
|
|
112
|
+
if (c >= 128 || BASE58_INDEXES[c] == -1) {
|
|
113
|
+
throw new SiweKeycloakException("Invalid Base58 character: " + c);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
int carry = BASE58_INDEXES[c];
|
|
117
|
+
for (int j = 0; j < outputLen || carry != 0; j++) {
|
|
118
|
+
carry += 58 * (j < outputLen ? b256[j] & 0xFF : 0);
|
|
119
|
+
b256[j] = (byte) (carry % 256);
|
|
120
|
+
carry /= 256;
|
|
121
|
+
if (j >= outputLen) {
|
|
122
|
+
outputLen = j + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip leading zeros in b256
|
|
128
|
+
int skipZeros = 0;
|
|
129
|
+
while (skipZeros < outputLen && b256[outputLen - 1 - skipZeros] == 0) {
|
|
130
|
+
skipZeros++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build result with leading zeros
|
|
134
|
+
byte[] result = new byte[zeros + outputLen - skipZeros];
|
|
135
|
+
for (int i = 0; i < zeros; i++) {
|
|
136
|
+
result[i] = 0;
|
|
137
|
+
}
|
|
138
|
+
for (int i = 0; i < outputLen - skipZeros; i++) {
|
|
139
|
+
result[zeros + i] = b256[outputLen - 1 - skipZeros - i];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Verifies an ed25519 signature for a SIWS message.
|
|
147
|
+
*
|
|
148
|
+
* @param message The message that was signed
|
|
149
|
+
* @param signature The Base58-encoded signature
|
|
150
|
+
* @param publicKey The Base58-encoded public key (Solana address)
|
|
151
|
+
* @return true if the signature is valid
|
|
152
|
+
* @throws SiweKeycloakException if verification fails
|
|
153
|
+
*/
|
|
154
|
+
public static boolean verifySignature(String message, String signature, String publicKey)
|
|
155
|
+
throws SiweKeycloakException {
|
|
156
|
+
try {
|
|
157
|
+
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
|
158
|
+
byte[] signatureBytes = decodeBase58(signature);
|
|
159
|
+
byte[] publicKeyBytes = decodeBase58(publicKey);
|
|
160
|
+
|
|
161
|
+
// Verify signature length (64 bytes for ed25519)
|
|
162
|
+
if (signatureBytes.length != 64) {
|
|
163
|
+
throw new SiweKeycloakException("Invalid signature length: expected 64 bytes, got " + signatureBytes.length);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Verify public key length (32 bytes for ed25519)
|
|
167
|
+
if (publicKeyBytes.length != 32) {
|
|
168
|
+
throw new SiweKeycloakException("Invalid public key length: expected 32 bytes, got " + publicKeyBytes.length);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Use TweetNaCl for ed25519 signature verification
|
|
172
|
+
// Create a Signature instance with the public key for verification
|
|
173
|
+
TweetNaclFast.Signature sig = new TweetNaclFast.Signature(publicKeyBytes, null);
|
|
174
|
+
return sig.detached_verify(messageBytes, signatureBytes);
|
|
175
|
+
} catch (SiweKeycloakException e) {
|
|
176
|
+
throw e;
|
|
177
|
+
} catch (Exception e) {
|
|
178
|
+
throw new SiweKeycloakException("Signature verification failed: " + e.getMessage());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Verifies SIWS credentials including signature, nonce, and domain.
|
|
184
|
+
*
|
|
185
|
+
* @param session Keycloak session
|
|
186
|
+
* @param configModel Authenticator config
|
|
187
|
+
* @param siwsCredentials SIWS credentials to verify
|
|
188
|
+
* @return true if credentials are valid
|
|
189
|
+
* @throws SiweKeycloakException if verification fails
|
|
190
|
+
*/
|
|
191
|
+
public static Boolean verifySiwsCredentials(KeycloakSession session,
|
|
192
|
+
AuthenticatorConfigModel configModel, SiwsCredentials siwsCredentials)
|
|
193
|
+
throws SiweKeycloakException {
|
|
194
|
+
HttpRequest httpRequest = session.getContext().getHttpRequest();
|
|
195
|
+
|
|
196
|
+
// Get and validate nonce
|
|
197
|
+
String nonce = getNonce(session);
|
|
198
|
+
if (nonce == null) {
|
|
199
|
+
throw new SiweKeycloakException("Missing nonce in session");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Verify nonce matches
|
|
203
|
+
if (!nonce.equals(siwsCredentials.message.getNonce())) {
|
|
204
|
+
throw new SiweKeycloakException("Nonce mismatch");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Reconstruct the message and verify signature
|
|
208
|
+
String reconstructedMessage = siwsCredentials.message.toMessage();
|
|
209
|
+
String address = siwsCredentials.message.getAddress();
|
|
210
|
+
|
|
211
|
+
return verifySignature(reconstructedMessage, siwsCredentials.signature, address);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Finds an existing user by wallet address or creates a new user if none exists.
|
|
216
|
+
*
|
|
217
|
+
* @param context Authentication flow context
|
|
218
|
+
* @param siwsCredentials SIWS credentials
|
|
219
|
+
* @return UserModel or null if creation/lookup fails
|
|
220
|
+
* @throws SiweKeycloakException if an error occurs
|
|
221
|
+
*/
|
|
222
|
+
public static UserModel findOrCreateUser(AuthenticationFlowContext context,
|
|
223
|
+
SiwsCredentials siwsCredentials) throws SiweKeycloakException {
|
|
224
|
+
return findOrCreateUser(context, siwsCredentials, true);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Finds an existing user by wallet address or creates a new user if none exists.
|
|
229
|
+
*
|
|
230
|
+
* @param context Authentication flow context
|
|
231
|
+
* @param siwsCredentials SIWS credentials
|
|
232
|
+
* @param failureChallenge Whether to issue a failure challenge on error
|
|
233
|
+
* @return UserModel or null if creation/lookup fails
|
|
234
|
+
* @throws SiweKeycloakException if an error occurs
|
|
235
|
+
*/
|
|
236
|
+
public static UserModel findOrCreateUser(AuthenticationFlowContext context,
|
|
237
|
+
SiwsCredentials siwsCredentials, Boolean failureChallenge) throws SiweKeycloakException {
|
|
238
|
+
String address = siwsCredentials.message.getAddress();
|
|
239
|
+
if (address == null) {
|
|
240
|
+
if (failureChallenge) {
|
|
241
|
+
context.getEvent().error(Errors.USER_NOT_FOUND);
|
|
242
|
+
LoginFormsProvider form = context.form();
|
|
243
|
+
form.setExecution(context.getExecution().getId());
|
|
244
|
+
form.setError(Messages.INVALID_USER);
|
|
245
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_USER, form.createLoginUsernamePassword());
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
UserModel user = null;
|
|
251
|
+
EntityManager entityManager = context.getSession().getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Look up existing user by Solana wallet address
|
|
255
|
+
List<SiweWalletAddress> wallets = entityManager
|
|
256
|
+
.createNamedQuery("findByRealmAddressType", SiweWalletAddress.class)
|
|
257
|
+
.setParameter("realmId", context.getRealm().getId())
|
|
258
|
+
.setParameter("address", address.trim())
|
|
259
|
+
.setParameter("addressType", SOLANA_ADDRESS_TYPE)
|
|
260
|
+
.getResultList();
|
|
261
|
+
|
|
262
|
+
if (!wallets.isEmpty()) {
|
|
263
|
+
user = context.getSession().users().getUserById(context.getRealm(), wallets.get(0).getUserId());
|
|
264
|
+
}
|
|
265
|
+
} catch (ModelDuplicateException mde) {
|
|
266
|
+
if (failureChallenge) {
|
|
267
|
+
ServicesLogger.LOGGER.modelDuplicateException(mde);
|
|
268
|
+
context.getEvent().error(Errors.USERNAME_IN_USE);
|
|
269
|
+
LoginFormsProvider form = context.form();
|
|
270
|
+
form.setError(Messages.USERNAME_EXISTS);
|
|
271
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_USER, form.createLoginUsernamePassword());
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If no existing user found, create a new one
|
|
277
|
+
if (user == null) {
|
|
278
|
+
if (verifySiwsCredentials(context.getSession(), context.getAuthenticatorConfig(), siwsCredentials)) {
|
|
279
|
+
KeycloakSession session = context.getSession();
|
|
280
|
+
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
|
281
|
+
MultivaluedMap<String, String> formData = new MultivaluedHashMap<>();
|
|
282
|
+
String username = generateRandomUsername(session, context.getRealm());
|
|
283
|
+
context.getEvent().detail(Details.USERNAME, username);
|
|
284
|
+
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
|
|
285
|
+
formData.add("username", username);
|
|
286
|
+
UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION, formData);
|
|
287
|
+
user = profile.create();
|
|
288
|
+
user.setEnabled(true);
|
|
289
|
+
|
|
290
|
+
// Add Solana wallet address
|
|
291
|
+
SiweWalletAddress wallet = new SiweWalletAddress();
|
|
292
|
+
wallet.setAddress(address.trim());
|
|
293
|
+
wallet.setAddressType(SOLANA_ADDRESS_TYPE);
|
|
294
|
+
wallet.setUserId(user.getId());
|
|
295
|
+
wallet.setRealmId(context.getRealm().getId());
|
|
296
|
+
entityManager.persist(wallet);
|
|
297
|
+
|
|
298
|
+
return user;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (failureChallenge) {
|
|
302
|
+
context.getEvent().error(Errors.USER_NOT_FOUND);
|
|
303
|
+
LoginFormsProvider form = context.form();
|
|
304
|
+
form.setExecution(context.getExecution().getId());
|
|
305
|
+
form.setError(Messages.INVALID_USER);
|
|
306
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_USER, form.createLoginUsernamePassword());
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
String username = user.getUsername();
|
|
312
|
+
context.getEvent().detail(Details.USERNAME, username);
|
|
313
|
+
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
|
|
314
|
+
return user;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Verifies that an existing user exists for the given wallet address.
|
|
319
|
+
*
|
|
320
|
+
* @param session Keycloak session
|
|
321
|
+
* @param configModel Authenticator config
|
|
322
|
+
* @param siwsCredentials SIWS credentials
|
|
323
|
+
* @return true if user exists
|
|
324
|
+
*/
|
|
325
|
+
public static Boolean verifyExistingUser(KeycloakSession session,
|
|
326
|
+
AuthenticatorConfigModel configModel, SiwsCredentials siwsCredentials) {
|
|
327
|
+
String address = siwsCredentials.message.getAddress().trim();
|
|
328
|
+
RealmModel realm = session.getContext().getRealm();
|
|
329
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
330
|
+
List<SiweWalletAddress> wallets = entityManager
|
|
331
|
+
.createNamedQuery("findByRealmAddressType", SiweWalletAddress.class)
|
|
332
|
+
.setParameter("realmId", realm.getId())
|
|
333
|
+
.setParameter("address", address)
|
|
334
|
+
.setParameter("addressType", SOLANA_ADDRESS_TYPE)
|
|
335
|
+
.getResultList();
|
|
336
|
+
return !wallets.isEmpty();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Performs full verification including existing user, credentials, etc.
|
|
341
|
+
*/
|
|
342
|
+
public static Boolean verify(KeycloakSession session, AuthenticatorConfigModel configModel,
|
|
343
|
+
SiwsCredentials siwsCredentials) throws SiweKeycloakException {
|
|
344
|
+
return verifyExistingUser(session, configModel, siwsCredentials)
|
|
345
|
+
&& verifySiwsCredentials(session, configModel, siwsCredentials);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generates a SIWS session with a new nonce.
|
|
350
|
+
*/
|
|
351
|
+
public static SiweSession generateSiwsSession(KeycloakSession session) {
|
|
352
|
+
String nonce = generateNonce();
|
|
353
|
+
String sessionKey = generateSessionKey(session.getContext().getRealm().getName());
|
|
354
|
+
setNonce(session, sessionKey, nonce);
|
|
355
|
+
return new SiweSession(sessionKey, nonce);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Gets the authenticator config for SIWS.
|
|
360
|
+
*/
|
|
361
|
+
public static AuthenticatorConfigModel getAuthenticatorConfig(KeycloakSession session, Boolean isDirectGrantFlow) {
|
|
362
|
+
RealmModel realm = session.getContext().getRealm();
|
|
363
|
+
AuthenticationFlowModel flow = realm.getBrowserFlow();
|
|
364
|
+
if (isDirectGrantFlow) {
|
|
365
|
+
flow = realm.getDirectGrantFlow();
|
|
366
|
+
}
|
|
367
|
+
AuthenticationExecutionModel execution = realm.getAuthenticationExecutionsStream(flow.getId())
|
|
368
|
+
.filter(e -> {
|
|
369
|
+
String authenticator = e.getAuthenticator();
|
|
370
|
+
return authenticator != null
|
|
371
|
+
&& authenticator.equals(isDirectGrantFlow ? SiwsDirectGrantAuthenticatorFactory.ID
|
|
372
|
+
: SiwsBrowserAuthenticatorFactory.ID);
|
|
373
|
+
})
|
|
374
|
+
.findFirst().orElse(null);
|
|
375
|
+
if (execution == null) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return realm.getAuthenticatorConfigById(execution.getAuthenticatorConfig());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Gets the session key from the SIWS cookie.
|
|
383
|
+
*/
|
|
384
|
+
public static String getSessionKey(KeycloakSession session) throws SiweKeycloakException {
|
|
385
|
+
HttpRequest request = session.getContext().getHttpRequest();
|
|
386
|
+
Cookie cookie = request.getHttpHeaders().getCookies().get(KEYCLOAK_SESSION_SIWS);
|
|
387
|
+
String sessionKey = null;
|
|
388
|
+
if (cookie != null) {
|
|
389
|
+
sessionKey = cookie.getValue();
|
|
390
|
+
}
|
|
391
|
+
if (sessionKey == null) {
|
|
392
|
+
throw new SiweKeycloakException("missing siws session");
|
|
393
|
+
}
|
|
394
|
+
return sessionKey;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Gets the nonce from the session.
|
|
399
|
+
*/
|
|
400
|
+
public static String getNonce(KeycloakSession session) throws SiweKeycloakException {
|
|
401
|
+
String sessionKey = getSessionKey(session);
|
|
402
|
+
return getNonce(session, sessionKey);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Gets the nonce for a specific session key.
|
|
407
|
+
*/
|
|
408
|
+
public static String getNonce(KeycloakSession session, String key) {
|
|
409
|
+
SiweNonce siweNonce = getSiweNonce(session, key);
|
|
410
|
+
if (siweNonce == null) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return siweNonce.getNonce();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Sets the nonce for a session key.
|
|
418
|
+
*/
|
|
419
|
+
public static String setNonce(KeycloakSession session, String key, String nonce) {
|
|
420
|
+
RealmModel realm = session.getContext().getRealm();
|
|
421
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
422
|
+
SiweNonce siweNonce = getSiweNonce(session, key);
|
|
423
|
+
if (siweNonce == null) {
|
|
424
|
+
siweNonce = new SiweNonce();
|
|
425
|
+
siweNonce.setKey(key);
|
|
426
|
+
siweNonce.setRealmId(realm.getId());
|
|
427
|
+
}
|
|
428
|
+
siweNonce.setNonce(nonce);
|
|
429
|
+
entityManager.merge(siweNonce);
|
|
430
|
+
return nonce;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Removes a nonce from the session.
|
|
435
|
+
*/
|
|
436
|
+
public static Void removeNonce(KeycloakSession session, String key) {
|
|
437
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
438
|
+
SiweNonce siweNonce = getSiweNonce(session, key);
|
|
439
|
+
if (siweNonce == null) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
entityManager.remove(siweNonce);
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Finds a user by Solana wallet address.
|
|
448
|
+
*/
|
|
449
|
+
public static UserModel findUserByWalletAddress(KeycloakSession session, String address) {
|
|
450
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
451
|
+
try {
|
|
452
|
+
TypedQuery<SiweWalletAddress> query = entityManager.createNamedQuery("findByRealmAddressType",
|
|
453
|
+
SiweWalletAddress.class);
|
|
454
|
+
query.setParameter("address", address.trim());
|
|
455
|
+
query.setParameter("realmId", session.getContext().getRealm().getId());
|
|
456
|
+
query.setParameter("addressType", SOLANA_ADDRESS_TYPE);
|
|
457
|
+
List<SiweWalletAddress> wallets = query.getResultList();
|
|
458
|
+
if (!wallets.isEmpty()) {
|
|
459
|
+
return session.users().getUserById(session.getContext().getRealm(), wallets.get(0).getUserId());
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
} catch (NoResultException e) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Adds a Solana wallet address to a user.
|
|
469
|
+
*/
|
|
470
|
+
public static boolean addWalletAddress(KeycloakSession session, UserModel user, String address) {
|
|
471
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
472
|
+
TypedQuery<SiweWalletAddress> query = entityManager.createNamedQuery("findByRealmAddressType",
|
|
473
|
+
SiweWalletAddress.class);
|
|
474
|
+
query.setParameter("address", address.trim());
|
|
475
|
+
query.setParameter("realmId", session.getContext().getRealm().getId());
|
|
476
|
+
query.setParameter("addressType", SOLANA_ADDRESS_TYPE);
|
|
477
|
+
List<SiweWalletAddress> existingWallets = query.getResultList();
|
|
478
|
+
for (SiweWalletAddress wallet : existingWallets) {
|
|
479
|
+
if (wallet.getUserId().equals(user.getId())) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
SiweWalletAddress wallet = new SiweWalletAddress();
|
|
484
|
+
wallet.setAddress(address.trim());
|
|
485
|
+
wallet.setAddressType(SOLANA_ADDRESS_TYPE);
|
|
486
|
+
wallet.setUserId(user.getId());
|
|
487
|
+
wallet.setRealmId(session.getContext().getRealm().getId());
|
|
488
|
+
entityManager.persist(wallet);
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Removes a Solana wallet address.
|
|
494
|
+
*/
|
|
495
|
+
public static boolean removeWalletAddress(KeycloakSession session, String address) {
|
|
496
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
497
|
+
TypedQuery<SiweWalletAddress> query = entityManager.createNamedQuery("findByRealmAddressType",
|
|
498
|
+
SiweWalletAddress.class);
|
|
499
|
+
query.setParameter("address", address.trim());
|
|
500
|
+
query.setParameter("realmId", session.getContext().getRealm().getId());
|
|
501
|
+
query.setParameter("addressType", SOLANA_ADDRESS_TYPE);
|
|
502
|
+
try {
|
|
503
|
+
List<SiweWalletAddress> wallets = query.getResultList();
|
|
504
|
+
if (!wallets.isEmpty()) {
|
|
505
|
+
entityManager.remove(wallets.get(0));
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
} catch (NoResultException e) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Gets all Solana wallet addresses for a user.
|
|
516
|
+
*/
|
|
517
|
+
public static List<SiweWalletAddress> getWalletAddresses(KeycloakSession session, UserModel user) {
|
|
518
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
519
|
+
TypedQuery<SiweWalletAddress> query = entityManager.createNamedQuery("findByUserId", SiweWalletAddress.class);
|
|
520
|
+
query.setParameter("userId", user.getId());
|
|
521
|
+
List<SiweWalletAddress> allWallets = query.getResultList();
|
|
522
|
+
// Filter to only Solana wallets
|
|
523
|
+
List<SiweWalletAddress> solanaWallets = new ArrayList<>();
|
|
524
|
+
for (SiweWalletAddress wallet : allWallets) {
|
|
525
|
+
if (SOLANA_ADDRESS_TYPE.equals(wallet.getAddressType())) {
|
|
526
|
+
solanaWallets.add(wallet);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return solanaWallets;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Private helper methods
|
|
533
|
+
|
|
534
|
+
private static SiweNonce getSiweNonce(KeycloakSession session, String key) {
|
|
535
|
+
RealmModel realm = session.getContext().getRealm();
|
|
536
|
+
EntityManager entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
|
537
|
+
SiweNonce siweNonce = entityManager.createNamedQuery("findByRealmKey", SiweNonce.class)
|
|
538
|
+
.setParameter("key", key)
|
|
539
|
+
.setParameter("realmId", realm.getId())
|
|
540
|
+
.getResultList().stream().findFirst().orElse(null);
|
|
541
|
+
return siweNonce;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private static String generateNonce() {
|
|
545
|
+
return Base64.getUrlEncoder().withoutPadding().encodeToString(random.generateSeed(8))
|
|
546
|
+
.replaceAll("[^A-Za-z0-9]", "0")
|
|
547
|
+
.substring(0, 8);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private static String generateSessionKey(String realmName) {
|
|
551
|
+
byte[] keyBytes = new byte[32];
|
|
552
|
+
random.nextBytes(keyBytes);
|
|
553
|
+
return realmName + "/" + (Base64.getEncoder().encodeToString(keyBytes));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private static String generateRandomUsername(KeycloakSession session, RealmModel realm) {
|
|
557
|
+
while (true) {
|
|
558
|
+
String username = String.format("sol_%010d", Math.abs(random.nextLong() % 10000000000L));
|
|
559
|
+
if (KeycloakModelUtils.findUserByNameOrEmail(session, realm, username) == null) {
|
|
560
|
+
return username;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticator.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Browser authenticator for Sign-In With Solana (SIWS).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
package com.bitspur.keycloak.siwe.siws.authenticators.browser;
|
|
13
|
+
|
|
14
|
+
import com.bitspur.keycloak.siwe.Errors.SiweKeycloakException;
|
|
15
|
+
import com.bitspur.keycloak.siwe.siws.SiwsCredentials;
|
|
16
|
+
import com.bitspur.keycloak.siwe.siws.SiwsUtil;
|
|
17
|
+
|
|
18
|
+
import jakarta.ws.rs.core.MultivaluedMap;
|
|
19
|
+
import jakarta.ws.rs.core.Response;
|
|
20
|
+
|
|
21
|
+
import org.jboss.logging.Logger;
|
|
22
|
+
import org.keycloak.authentication.AuthenticationFlowContext;
|
|
23
|
+
import org.keycloak.authentication.AuthenticationFlowError;
|
|
24
|
+
import org.keycloak.authentication.Authenticator;
|
|
25
|
+
import org.keycloak.forms.login.LoginFormsProvider;
|
|
26
|
+
import org.keycloak.models.KeycloakSession;
|
|
27
|
+
import org.keycloak.models.RealmModel;
|
|
28
|
+
import org.keycloak.models.UserModel;
|
|
29
|
+
import org.keycloak.services.messages.Messages;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Browser-based SIWS authenticator.
|
|
33
|
+
* Handles the login form submission with Solana wallet signature.
|
|
34
|
+
*/
|
|
35
|
+
public class SiwsBrowserAuthenticator implements Authenticator {
|
|
36
|
+
private static final Logger logger = Logger.getLogger(SiwsBrowserAuthenticator.class);
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
public void authenticate(AuthenticationFlowContext context) {
|
|
40
|
+
// Display the SIWS login form
|
|
41
|
+
LoginFormsProvider form = context.form();
|
|
42
|
+
form.setExecution(context.getExecution().getId());
|
|
43
|
+
Response response = form.createLoginUsernamePassword();
|
|
44
|
+
context.challenge(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Override
|
|
48
|
+
public void action(AuthenticationFlowContext context) {
|
|
49
|
+
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
50
|
+
String signature = formData.getFirst("username"); // Signature is passed as username
|
|
51
|
+
String message = formData.getFirst("password"); // Base64 SIWS message is passed as password
|
|
52
|
+
|
|
53
|
+
if (signature == null || signature.isEmpty() || message == null || message.isEmpty()) {
|
|
54
|
+
context.getEvent().error("missing_credentials");
|
|
55
|
+
LoginFormsProvider form = context.form();
|
|
56
|
+
form.setExecution(context.getExecution().getId());
|
|
57
|
+
form.setError(Messages.INVALID_USER);
|
|
58
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, form.createLoginUsernamePassword());
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
SiwsCredentials siwsCredentials = new SiwsCredentials(signature, message);
|
|
64
|
+
UserModel user = SiwsUtil.findOrCreateUser(context, siwsCredentials, true);
|
|
65
|
+
|
|
66
|
+
if (user == null) {
|
|
67
|
+
// findOrCreateUser already issued failure challenge
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify the credentials
|
|
72
|
+
if (!SiwsUtil.verifySiwsCredentials(context.getSession(), context.getAuthenticatorConfig(), siwsCredentials)) {
|
|
73
|
+
context.getEvent().error("invalid_signature");
|
|
74
|
+
LoginFormsProvider form = context.form();
|
|
75
|
+
form.setExecution(context.getExecution().getId());
|
|
76
|
+
form.setError(Messages.INVALID_USER);
|
|
77
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, form.createLoginUsernamePassword());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
context.setUser(user);
|
|
82
|
+
context.success();
|
|
83
|
+
} catch (SiweKeycloakException e) {
|
|
84
|
+
logger.error("SIWS authentication error", e);
|
|
85
|
+
context.getEvent().error("siws_error");
|
|
86
|
+
LoginFormsProvider form = context.form();
|
|
87
|
+
form.setExecution(context.getExecution().getId());
|
|
88
|
+
form.setError(e.getMessage());
|
|
89
|
+
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, form.createLoginUsernamePassword());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Override
|
|
94
|
+
public boolean requiresUser() {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Override
|
|
99
|
+
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Override
|
|
104
|
+
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
105
|
+
// No required actions
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Override
|
|
109
|
+
public void close() {
|
|
110
|
+
// Nothing to close
|
|
111
|
+
}
|
|
112
|
+
}
|