@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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/SiwsCredentials.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.bitspur.keycloak.siwe.siws;
|
|
11
|
+
|
|
12
|
+
import com.bitspur.keycloak.siwe.Errors.SiweKeycloakException;
|
|
13
|
+
import java.util.Base64;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Holds SIWS credentials: the parsed message and the signature.
|
|
17
|
+
*
|
|
18
|
+
* The signature is a Base58-encoded ed25519 signature.
|
|
19
|
+
* The message is a Base64-encoded SIWS message string.
|
|
20
|
+
*/
|
|
21
|
+
public class SiwsCredentials {
|
|
22
|
+
private static final Base64.Decoder decoder = Base64.getDecoder();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The parsed SIWS message.
|
|
26
|
+
*/
|
|
27
|
+
public SiwsMessage message;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The Base58-encoded ed25519 signature.
|
|
31
|
+
*/
|
|
32
|
+
public String signature;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates new SIWS credentials.
|
|
36
|
+
*
|
|
37
|
+
* @param username The signature (Base58 encoded)
|
|
38
|
+
* @param password The SIWS message (Base64 encoded)
|
|
39
|
+
* @throws SiweKeycloakException If the message cannot be parsed
|
|
40
|
+
*/
|
|
41
|
+
public SiwsCredentials(String username, String password) throws SiweKeycloakException {
|
|
42
|
+
this.signature = username;
|
|
43
|
+
try {
|
|
44
|
+
String decodedMessage = new String(decoder.decode(password));
|
|
45
|
+
this.message = new SiwsMessage.Parser().parse(decodedMessage);
|
|
46
|
+
} catch (SiwsParseException e) {
|
|
47
|
+
throw new SiweKeycloakException(e.getMessage());
|
|
48
|
+
} catch (IllegalArgumentException e) {
|
|
49
|
+
throw new SiweKeycloakException("Invalid Base64 encoding for SIWS message");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/SiwsMessage.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*
|
|
9
|
+
* Sign-In With Solana (SIWS) message parser
|
|
10
|
+
* Based on EIP-4361 / CAIP-122 specification adapted for Solana
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
package com.bitspur.keycloak.siwe.siws;
|
|
14
|
+
|
|
15
|
+
import java.time.OffsetDateTime;
|
|
16
|
+
import java.time.format.DateTimeFormatter;
|
|
17
|
+
import java.time.format.DateTimeParseException;
|
|
18
|
+
import java.util.ArrayList;
|
|
19
|
+
import java.util.List;
|
|
20
|
+
import java.util.regex.Matcher;
|
|
21
|
+
import java.util.regex.Pattern;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a parsed SIWS (Sign-In With Solana) message.
|
|
25
|
+
*
|
|
26
|
+
* SIWS message format follows EIP-4361 / CAIP-122:
|
|
27
|
+
* <pre>
|
|
28
|
+
* ${domain} wants you to sign in with your Solana account:
|
|
29
|
+
* ${address}
|
|
30
|
+
*
|
|
31
|
+
* ${statement}
|
|
32
|
+
*
|
|
33
|
+
* URI: ${uri}
|
|
34
|
+
* Version: ${version}
|
|
35
|
+
* Chain ID: ${chainId}
|
|
36
|
+
* Nonce: ${nonce}
|
|
37
|
+
* Issued At: ${issuedAt}
|
|
38
|
+
* Expiration Time: ${expirationTime}
|
|
39
|
+
* Not Before: ${notBefore}
|
|
40
|
+
* Request ID: ${requestId}
|
|
41
|
+
* Resources:
|
|
42
|
+
* - ${resource1}
|
|
43
|
+
* - ${resource2}
|
|
44
|
+
* </pre>
|
|
45
|
+
*/
|
|
46
|
+
public class SiwsMessage {
|
|
47
|
+
// Solana address: Base58 encoded, 32-44 characters
|
|
48
|
+
private static final Pattern SOLANA_ADDRESS_PATTERN = Pattern.compile("^[1-9A-HJ-NP-Za-km-z]{32,44}$");
|
|
49
|
+
|
|
50
|
+
private String domain;
|
|
51
|
+
private String address;
|
|
52
|
+
private String statement;
|
|
53
|
+
private String uri;
|
|
54
|
+
private String version;
|
|
55
|
+
private String chainId;
|
|
56
|
+
private String nonce;
|
|
57
|
+
private OffsetDateTime issuedAt;
|
|
58
|
+
private OffsetDateTime expirationTime;
|
|
59
|
+
private OffsetDateTime notBefore;
|
|
60
|
+
private String requestId;
|
|
61
|
+
private List<String> resources;
|
|
62
|
+
|
|
63
|
+
private SiwsMessage() {
|
|
64
|
+
this.resources = new ArrayList<>();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public String getDomain() {
|
|
68
|
+
return domain;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public String getAddress() {
|
|
72
|
+
return address;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public String getStatement() {
|
|
76
|
+
return statement;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public String getUri() {
|
|
80
|
+
return uri;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public String getVersion() {
|
|
84
|
+
return version;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public String getChainId() {
|
|
88
|
+
return chainId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public String getNonce() {
|
|
92
|
+
return nonce;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public OffsetDateTime getIssuedAt() {
|
|
96
|
+
return issuedAt;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public OffsetDateTime getExpirationTime() {
|
|
100
|
+
return expirationTime;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public OffsetDateTime getNotBefore() {
|
|
104
|
+
return notBefore;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public String getRequestId() {
|
|
108
|
+
return requestId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public List<String> getResources() {
|
|
112
|
+
return resources;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validates that the Solana address is in the correct format.
|
|
117
|
+
* Solana addresses are Base58-encoded Ed25519 public keys.
|
|
118
|
+
*/
|
|
119
|
+
public static boolean isValidSolanaAddress(String address) {
|
|
120
|
+
if (address == null || address.isEmpty()) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return SOLANA_ADDRESS_PATTERN.matcher(address).matches();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parser for SIWS messages.
|
|
128
|
+
*/
|
|
129
|
+
public static class Parser {
|
|
130
|
+
// ABNF grammar patterns based on EIP-4361 / CAIP-122
|
|
131
|
+
private static final Pattern DOMAIN_PATTERN = Pattern.compile(
|
|
132
|
+
"^(.+) wants you to sign in with your Solana account:$",
|
|
133
|
+
Pattern.MULTILINE
|
|
134
|
+
);
|
|
135
|
+
private static final Pattern ADDRESS_PATTERN = Pattern.compile(
|
|
136
|
+
"^([1-9A-HJ-NP-Za-km-z]{32,44})$",
|
|
137
|
+
Pattern.MULTILINE
|
|
138
|
+
);
|
|
139
|
+
private static final Pattern URI_PATTERN = Pattern.compile("^URI: (.+)$", Pattern.MULTILINE);
|
|
140
|
+
private static final Pattern VERSION_PATTERN = Pattern.compile("^Version: (.+)$", Pattern.MULTILINE);
|
|
141
|
+
private static final Pattern CHAIN_ID_PATTERN = Pattern.compile("^Chain ID: (.+)$", Pattern.MULTILINE);
|
|
142
|
+
private static final Pattern NONCE_PATTERN = Pattern.compile("^Nonce: ([A-Za-z0-9]{8,})$", Pattern.MULTILINE);
|
|
143
|
+
private static final Pattern ISSUED_AT_PATTERN = Pattern.compile("^Issued At: (.+)$", Pattern.MULTILINE);
|
|
144
|
+
private static final Pattern EXPIRATION_TIME_PATTERN = Pattern.compile("^Expiration Time: (.+)$", Pattern.MULTILINE);
|
|
145
|
+
private static final Pattern NOT_BEFORE_PATTERN = Pattern.compile("^Not Before: (.+)$", Pattern.MULTILINE);
|
|
146
|
+
private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("^Request ID: (.+)$", Pattern.MULTILINE);
|
|
147
|
+
private static final Pattern RESOURCES_PATTERN = Pattern.compile("^Resources:$", Pattern.MULTILINE);
|
|
148
|
+
private static final Pattern RESOURCE_ITEM_PATTERN = Pattern.compile("^- (.+)$", Pattern.MULTILINE);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parses a SIWS message string into a SiwsMessage object.
|
|
152
|
+
*
|
|
153
|
+
* @param message The raw SIWS message string
|
|
154
|
+
* @return Parsed SiwsMessage
|
|
155
|
+
* @throws SiwsParseException If the message cannot be parsed
|
|
156
|
+
*/
|
|
157
|
+
public SiwsMessage parse(String message) throws SiwsParseException {
|
|
158
|
+
if (message == null || message.trim().isEmpty()) {
|
|
159
|
+
throw new SiwsParseException("Message cannot be null or empty");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
SiwsMessage siws = new SiwsMessage();
|
|
163
|
+
String[] lines = message.split("\n");
|
|
164
|
+
|
|
165
|
+
if (lines.length < 2) {
|
|
166
|
+
throw new SiwsParseException("Invalid SIWS message format: too few lines");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Parse domain from first line
|
|
170
|
+
Matcher domainMatcher = DOMAIN_PATTERN.matcher(lines[0]);
|
|
171
|
+
if (!domainMatcher.matches()) {
|
|
172
|
+
throw new SiwsParseException("Invalid SIWS message: missing or invalid domain line");
|
|
173
|
+
}
|
|
174
|
+
siws.domain = domainMatcher.group(1).trim();
|
|
175
|
+
|
|
176
|
+
// Parse address from second line
|
|
177
|
+
if (lines.length < 2 || !ADDRESS_PATTERN.matcher(lines[1].trim()).matches()) {
|
|
178
|
+
throw new SiwsParseException("Invalid SIWS message: missing or invalid Solana address");
|
|
179
|
+
}
|
|
180
|
+
siws.address = lines[1].trim();
|
|
181
|
+
|
|
182
|
+
// Parse statement (optional, between address and first field)
|
|
183
|
+
StringBuilder statementBuilder = new StringBuilder();
|
|
184
|
+
int fieldStartIndex = -1;
|
|
185
|
+
for (int i = 2; i < lines.length; i++) {
|
|
186
|
+
String line = lines[i];
|
|
187
|
+
if (line.startsWith("URI:") || line.startsWith("Version:") ||
|
|
188
|
+
line.startsWith("Chain ID:") || line.startsWith("Nonce:") ||
|
|
189
|
+
line.startsWith("Issued At:") || line.startsWith("Expiration Time:") ||
|
|
190
|
+
line.startsWith("Not Before:") || line.startsWith("Request ID:") ||
|
|
191
|
+
line.startsWith("Resources:")) {
|
|
192
|
+
fieldStartIndex = i;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
// Skip empty lines at the start/end of statement
|
|
196
|
+
if (statementBuilder.length() > 0 || !line.trim().isEmpty()) {
|
|
197
|
+
if (statementBuilder.length() > 0) {
|
|
198
|
+
statementBuilder.append("\n");
|
|
199
|
+
}
|
|
200
|
+
statementBuilder.append(line);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
String statement = statementBuilder.toString().trim();
|
|
204
|
+
if (!statement.isEmpty()) {
|
|
205
|
+
siws.statement = statement;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Parse remaining fields
|
|
209
|
+
String remainingMessage = fieldStartIndex > 0
|
|
210
|
+
? String.join("\n", java.util.Arrays.copyOfRange(lines, fieldStartIndex, lines.length))
|
|
211
|
+
: "";
|
|
212
|
+
|
|
213
|
+
// Required fields
|
|
214
|
+
siws.uri = extractField(remainingMessage, URI_PATTERN, "URI");
|
|
215
|
+
siws.version = extractField(remainingMessage, VERSION_PATTERN, "Version");
|
|
216
|
+
siws.nonce = extractField(remainingMessage, NONCE_PATTERN, "Nonce");
|
|
217
|
+
siws.issuedAt = parseDateTime(extractField(remainingMessage, ISSUED_AT_PATTERN, "Issued At"));
|
|
218
|
+
|
|
219
|
+
// Optional fields
|
|
220
|
+
String chainIdStr = extractFieldOptional(remainingMessage, CHAIN_ID_PATTERN);
|
|
221
|
+
if (chainIdStr != null) {
|
|
222
|
+
siws.chainId = chainIdStr;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
String expirationTimeStr = extractFieldOptional(remainingMessage, EXPIRATION_TIME_PATTERN);
|
|
226
|
+
if (expirationTimeStr != null) {
|
|
227
|
+
siws.expirationTime = parseDateTime(expirationTimeStr);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
String notBeforeStr = extractFieldOptional(remainingMessage, NOT_BEFORE_PATTERN);
|
|
231
|
+
if (notBeforeStr != null) {
|
|
232
|
+
siws.notBefore = parseDateTime(notBeforeStr);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
String requestIdStr = extractFieldOptional(remainingMessage, REQUEST_ID_PATTERN);
|
|
236
|
+
if (requestIdStr != null) {
|
|
237
|
+
siws.requestId = requestIdStr;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Parse resources (optional)
|
|
241
|
+
Matcher resourcesMatcher = RESOURCES_PATTERN.matcher(remainingMessage);
|
|
242
|
+
if (resourcesMatcher.find()) {
|
|
243
|
+
int resourcesStart = resourcesMatcher.end();
|
|
244
|
+
String resourcesSection = remainingMessage.substring(resourcesStart);
|
|
245
|
+
Matcher resourceItemMatcher = RESOURCE_ITEM_PATTERN.matcher(resourcesSection);
|
|
246
|
+
while (resourceItemMatcher.find()) {
|
|
247
|
+
siws.resources.add(resourceItemMatcher.group(1).trim());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate the message
|
|
252
|
+
validate(siws);
|
|
253
|
+
|
|
254
|
+
return siws;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private String extractField(String message, Pattern pattern, String fieldName) throws SiwsParseException {
|
|
258
|
+
Matcher matcher = pattern.matcher(message);
|
|
259
|
+
if (!matcher.find()) {
|
|
260
|
+
throw new SiwsParseException("Missing required field: " + fieldName);
|
|
261
|
+
}
|
|
262
|
+
return matcher.group(1).trim();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private String extractFieldOptional(String message, Pattern pattern) {
|
|
266
|
+
Matcher matcher = pattern.matcher(message);
|
|
267
|
+
if (matcher.find()) {
|
|
268
|
+
return matcher.group(1).trim();
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private OffsetDateTime parseDateTime(String dateTimeStr) throws SiwsParseException {
|
|
274
|
+
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return OffsetDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
279
|
+
} catch (DateTimeParseException e) {
|
|
280
|
+
// Try parsing without timezone offset
|
|
281
|
+
try {
|
|
282
|
+
return OffsetDateTime.parse(dateTimeStr + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
283
|
+
} catch (DateTimeParseException e2) {
|
|
284
|
+
throw new SiwsParseException("Invalid datetime format: " + dateTimeStr);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private void validate(SiwsMessage siws) throws SiwsParseException {
|
|
290
|
+
// Validate Solana address format
|
|
291
|
+
if (!SiwsMessage.isValidSolanaAddress(siws.address)) {
|
|
292
|
+
throw new SiwsParseException("Invalid Solana address format: " + siws.address);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Validate version is "1"
|
|
296
|
+
if (!"1".equals(siws.version)) {
|
|
297
|
+
throw new SiwsParseException("Unsupported SIWS version: " + siws.version + " (expected: 1)");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Validate nonce length (at least 8 alphanumeric characters)
|
|
301
|
+
if (siws.nonce == null || siws.nonce.length() < 8) {
|
|
302
|
+
throw new SiwsParseException("Nonce must be at least 8 alphanumeric characters");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Validate expiration time if present
|
|
306
|
+
if (siws.expirationTime != null && siws.expirationTime.isBefore(OffsetDateTime.now())) {
|
|
307
|
+
throw new SiwsParseException("Message has expired");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Validate not before if present
|
|
311
|
+
if (siws.notBefore != null && siws.notBefore.isAfter(OffsetDateTime.now())) {
|
|
312
|
+
throw new SiwsParseException("Message is not yet valid (notBefore is in the future)");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Reconstructs the SIWS message from its components.
|
|
319
|
+
* Used for signature verification to ensure message integrity.
|
|
320
|
+
*/
|
|
321
|
+
public String toMessage() {
|
|
322
|
+
StringBuilder sb = new StringBuilder();
|
|
323
|
+
sb.append(domain).append(" wants you to sign in with your Solana account:\n");
|
|
324
|
+
sb.append(address).append("\n");
|
|
325
|
+
|
|
326
|
+
if (statement != null && !statement.isEmpty()) {
|
|
327
|
+
sb.append("\n").append(statement).append("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
sb.append("\n");
|
|
331
|
+
sb.append("URI: ").append(uri).append("\n");
|
|
332
|
+
sb.append("Version: ").append(version).append("\n");
|
|
333
|
+
|
|
334
|
+
if (chainId != null) {
|
|
335
|
+
sb.append("Chain ID: ").append(chainId).append("\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
sb.append("Nonce: ").append(nonce).append("\n");
|
|
339
|
+
sb.append("Issued At: ").append(issuedAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
|
|
340
|
+
|
|
341
|
+
if (expirationTime != null) {
|
|
342
|
+
sb.append("Expiration Time: ").append(expirationTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (notBefore != null) {
|
|
346
|
+
sb.append("Not Before: ").append(notBefore.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (requestId != null) {
|
|
350
|
+
sb.append("Request ID: ").append(requestId).append("\n");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (resources != null && !resources.isEmpty()) {
|
|
354
|
+
sb.append("Resources:\n");
|
|
355
|
+
for (String resource : resources) {
|
|
356
|
+
sb.append("- ").append(resource).append("\n");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return sb.toString();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /src/main/java/com/bitspur/keycloak/siwe/siws/SiwsParseException.java
|
|
3
|
+
* Project: siwe-keycloak
|
|
4
|
+
* File Created: 24-01-2026
|
|
5
|
+
* Author: Clay Risser
|
|
6
|
+
* -----
|
|
7
|
+
* BitSpur (c) Copyright 2022 - 2026
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.bitspur.keycloak.siwe.siws;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Exception thrown when parsing a SIWS message fails.
|
|
14
|
+
*/
|
|
15
|
+
public class SiwsParseException extends Exception {
|
|
16
|
+
public SiwsParseException(String message) {
|
|
17
|
+
super(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public SiwsParseException(String message, Throwable cause) {
|
|
21
|
+
super(message, cause);
|
|
22
|
+
}
|
|
23
|
+
}
|