Skip to content

Commit aacdf80

Browse files
authored
Add shim for Web Crypto API to admin and account console (#33480)
Closes #33330 Signed-off-by: Jon Koops <[email protected]>
1 parent e8d8de8 commit aacdf80

File tree

13 files changed

+96
-23
lines changed

13 files changed

+96
-23
lines changed

services/src/main/java/org/keycloak/cookie/SecureContextResolver.java renamed to common/src/main/java/org/keycloak/common/util/SecureContextResolver.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package org.keycloak.cookie;
1+
package org.keycloak.common.util;
22

33
import java.net.URI;
44
import java.util.regex.Pattern;
55

6-
class SecureContextResolver {
6+
public class SecureContextResolver {
77

88
private static final Pattern LOCALHOST_IPV4 = Pattern.compile("127.\\d{1,3}.\\d{1,3}.\\d{1,3}");
99

@@ -15,7 +15,7 @@ class SecureContextResolver {
1515
* @param uri The URI to check.
1616
* @return Whether the URI can be considered potentially trustworthy.
1717
*/
18-
static boolean isSecureContext(URI uri) {
18+
public static boolean isSecureContext(URI uri) {
1919
if (uri.getScheme().equals("https")) {
2020
return true;
2121
}

services/src/test/java/org/keycloak/cookie/SecureContextResolverTest.java renamed to common/src/test/java/org/keycloak/common/util/SecureContextResolverTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.keycloak.cookie;
1+
package org.keycloak.common.util;
22

33
import org.junit.Assert;
44
import org.junit.Test;

docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@ const registerUrl = await keycloak.createRegisterUrl();
441441

442442
Make sure to update your code to `await` these methods.
443443

444+
== A secure context is now required
445+
446+
Keycloak JS now requires a link:https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts[secure context] to run. The reason for this is that the library now uses the Web Crypto API to calculate the SHA-256 digests needed to support PKCE. This API is only available in secure contexts, which are contexts that are served over HTTPS, `localhost` or a `.localhost` domain. If you are using the library in a non-secure context you'll need to update your development environment to use a secure context.
447+
444448
= Stricter startup behavior for build-time options
445449

446450
When the provided build-time options differ at startup from the values persisted in the server image during the last optimized {project_name} build, {project_name} will now fail to start. Previously, a warning message was displayed in such cases.

js/apps/account-ui/maven-resources/theme/keycloak.v3/account/index.ftl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
}
5858
}
5959
</script>
60+
<#if !isSecureContext>
61+
<script type="module" src="${resourceCommonUrl}/vendor/web-crypto-shim/web-crypto-shim.js"></script>
62+
</#if>
6063
<#if devServerUrl?has_content>
6164
<script type="module">
6265
import { injectIntoGlobalHook } from "${devServerUrl}/@react-refresh";

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/index.ftl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
}
5858
}
5959
</script>
60+
<#if !isSecureContext>
61+
<script type="module" src="${resourceCommonUrl}/vendor/web-crypto-shim/web-crypto-shim.js"></script>
62+
</#if>
6063
<#if devServerUrl?has_content>
6164
<script type="module">
6265
import { injectIntoGlobalHook } from "${devServerUrl}/@react-refresh";

js/libs/keycloak-js/lib/keycloak.js

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ function Keycloak (config) {
5252
var logInfo = createLogger(console.info);
5353
var logWarn = createLogger(console.warn);
5454

55+
if (!globalThis.isSecureContext) {
56+
logWarn('[KEYCLOAK] Keycloak JS should only be used in a secure context: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts');
57+
}
58+
5559
kc.init = function (initOptions) {
5660
if (kc.didInitialize) {
5761
throw new Error("A 'Keycloak' instance can only be initialized once.");
@@ -333,20 +337,12 @@ function Keycloak (config) {
333337
}
334338

335339
function generateRandomData(len) {
336-
// use web crypto APIs if possible
337-
var array = null;
338-
var crypto = window.crypto || window.msCrypto;
339-
if (crypto && crypto.getRandomValues && window.Uint8Array) {
340-
array = new Uint8Array(len);
341-
crypto.getRandomValues(array);
342-
return array;
340+
if (typeof crypto === "undefined" || typeof crypto.getRandomValues === "undefined") {
341+
throw new Error("Web Crypto API is not available.");
343342
}
344343

345-
// fallback to Math random
346-
array = new Array(len);
347-
for (var j = 0; j < array.length; j++) {
348-
array[j] = Math.floor(256 * Math.random());
349-
}
344+
const array = new Uint8Array(len);
345+
crypto.getRandomValues(array);
350346
return array;
351347
}
352348

@@ -465,14 +461,16 @@ function Keycloak (config) {
465461
}
466462

467463
if (kc.pkceMethod) {
468-
if (!globalThis.isSecureContext) {
469-
logWarn('[KEYCLOAK] PKCE is only supported in secure contexts (HTTPS)');
470-
} else {
471-
var codeVerifier = generateCodeVerifier(96);
464+
try {
465+
const codeVerifier = generateCodeVerifier(96);
466+
const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);
467+
472468
callbackState.pkceCodeVerifier = codeVerifier;
473-
var pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);
469+
474470
url += '&code_challenge=' + pkceChallenge;
475471
url += '&code_challenge_method=' + kc.pkceMethod;
472+
} catch (error) {
473+
throw new Error("Failed to generate PKCE challenge.", { cause: error });
476474
}
477475
}
478476

@@ -1741,8 +1739,12 @@ function bytesToBase64(bytes) {
17411739
async function sha256Digest(message) {
17421740
const encoder = new TextEncoder();
17431741
const data = encoder.encode(message);
1744-
const hash = await crypto.subtle.digest("SHA-256", data);
1745-
return hash;
1742+
1743+
if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") {
1744+
throw new Error("Web Crypto API is not available.");
1745+
}
1746+
1747+
return await crypto.subtle.digest("SHA-256", data);
17461748
}
17471749

17481750
/**

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import jakarta.ws.rs.core.Cookie;
44
import jakarta.ws.rs.core.NewCookie;
55
import org.jboss.logging.Logger;
6+
import org.keycloak.common.util.SecureContextResolver;
67
import org.keycloak.models.KeycloakContext;
78

89
import java.util.Map;
10+
911
public class DefaultCookieProvider implements CookieProvider {
1012

1113
private static final Logger logger = Logger.getLogger(DefaultCookieProvider.class);

services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.keycloak.common.Profile;
1010
import org.keycloak.common.Version;
1111
import org.keycloak.common.util.Environment;
12+
import org.keycloak.common.util.SecureContextResolver;
1213
import org.keycloak.models.AccountRoles;
1314
import org.keycloak.models.ClientModel;
1415
import org.keycloak.models.Constants;
@@ -109,6 +110,9 @@ public Response getMainPage() throws IOException, FreeMarkerException {
109110
.path("/")
110111
.build(realm);
111112

113+
final var isSecureContext = SecureContextResolver.isSecureContext(serverBaseUri);
114+
115+
map.put("isSecureContext", isSecureContext);
112116
map.put("serverBaseUrl", serverBaseUrl);
113117
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
114118
// Note that these should be removed from the template of the Account Console as well.

services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.keycloak.common.Profile;
3434
import org.keycloak.common.Version;
3535
import org.keycloak.common.util.Environment;
36+
import org.keycloak.common.util.SecureContextResolver;
3637
import org.keycloak.common.util.UriUtils;
3738
import org.keycloak.headers.SecurityHeadersProvider;
3839
import org.keycloak.http.HttpRequest;
@@ -347,7 +348,9 @@ public Response getMainPage() throws IOException, FreeMarkerException {
347348

348349
final var map = new HashMap<String, Object>();
349350
final var theme = AdminRoot.getTheme(session, realm);
351+
final var isSecureContext = SecureContextResolver.isSecureContext(adminBaseUri);
350352

353+
map.put("isSecureContext", isSecureContext);
351354
map.put("serverBaseUrl", serverBaseUrl);
352355
map.put("adminBaseUrl", adminBaseUrl);
353356
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.

0 commit comments

Comments
 (0)