Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.keycloak.cookie;
package org.keycloak.common.util;

import java.net.URI;
import java.util.regex.Pattern;

class SecureContextResolver {
public class SecureContextResolver {

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

Expand All @@ -15,7 +15,7 @@ class SecureContextResolver {
* @param uri The URI to check.
* @return Whether the URI can be considered potentially trustworthy.
*/
static boolean isSecureContext(URI uri) {
public static boolean isSecureContext(URI uri) {
if (uri.getScheme().equals("https")) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.keycloak.cookie;
package org.keycloak.common.util;

import org.junit.Assert;
import org.junit.Test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ const registerUrl = await keycloak.createRegisterUrl();

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

== A secure context is now required

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.

= Stricter startup behavior for build-time options

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.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
}
}
</script>
<#if !isSecureContext>
<script type="module" src="${resourceCommonUrl}/vendor/web-crypto-shim/web-crypto-shim.js"></script>
</#if>
<#if devServerUrl?has_content>
<script type="module">
import { injectIntoGlobalHook } from "${devServerUrl}/@react-refresh";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
}
}
</script>
<#if !isSecureContext>
<script type="module" src="${resourceCommonUrl}/vendor/web-crypto-shim/web-crypto-shim.js"></script>
</#if>
<#if devServerUrl?has_content>
<script type="module">
import { injectIntoGlobalHook } from "${devServerUrl}/@react-refresh";
Expand Down
40 changes: 21 additions & 19 deletions js/libs/keycloak-js/lib/keycloak.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ function Keycloak (config) {
var logInfo = createLogger(console.info);
var logWarn = createLogger(console.warn);

if (!globalThis.isSecureContext) {
logWarn('[KEYCLOAK] Keycloak JS should only be used in a secure context: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts');
}

kc.init = function (initOptions) {
if (kc.didInitialize) {
throw new Error("A 'Keycloak' instance can only be initialized once.");
Expand Down Expand Up @@ -333,20 +337,12 @@ function Keycloak (config) {
}

function generateRandomData(len) {
// use web crypto APIs if possible
var array = null;
var crypto = window.crypto || window.msCrypto;
if (crypto && crypto.getRandomValues && window.Uint8Array) {
array = new Uint8Array(len);
crypto.getRandomValues(array);
return array;
if (typeof crypto === "undefined" || typeof crypto.getRandomValues === "undefined") {
throw new Error("Web Crypto API is not available.");
}

// fallback to Math random
array = new Array(len);
for (var j = 0; j < array.length; j++) {
array[j] = Math.floor(256 * Math.random());
}
const array = new Uint8Array(len);
crypto.getRandomValues(array);
return array;
}

Expand Down Expand Up @@ -465,14 +461,16 @@ function Keycloak (config) {
}

if (kc.pkceMethod) {
if (!globalThis.isSecureContext) {
logWarn('[KEYCLOAK] PKCE is only supported in secure contexts (HTTPS)');
} else {
var codeVerifier = generateCodeVerifier(96);
try {
const codeVerifier = generateCodeVerifier(96);
const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);

callbackState.pkceCodeVerifier = codeVerifier;
var pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);

url += '&code_challenge=' + pkceChallenge;
url += '&code_challenge_method=' + kc.pkceMethod;
} catch (error) {
throw new Error("Failed to generate PKCE challenge.", { cause: error });
}
}

Expand Down Expand Up @@ -1741,8 +1739,12 @@ function bytesToBase64(bytes) {
async function sha256Digest(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hash = await crypto.subtle.digest("SHA-256", data);
return hash;

if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") {
throw new Error("Web Crypto API is not available.");
}

return await crypto.subtle.digest("SHA-256", data);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;
import org.jboss.logging.Logger;
import org.keycloak.common.util.SecureContextResolver;
import org.keycloak.models.KeycloakContext;

import java.util.Map;

public class DefaultCookieProvider implements CookieProvider {

private static final Logger logger = Logger.getLogger(DefaultCookieProvider.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.keycloak.common.Profile;
import org.keycloak.common.Version;
import org.keycloak.common.util.Environment;
import org.keycloak.common.util.SecureContextResolver;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
Expand Down Expand Up @@ -109,6 +110,9 @@ public Response getMainPage() throws IOException, FreeMarkerException {
.path("/")
.build(realm);

final var isSecureContext = SecureContextResolver.isSecureContext(serverBaseUri);

map.put("isSecureContext", isSecureContext);
map.put("serverBaseUrl", serverBaseUrl);
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
// Note that these should be removed from the template of the Account Console as well.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.keycloak.common.Profile;
import org.keycloak.common.Version;
import org.keycloak.common.util.Environment;
import org.keycloak.common.util.SecureContextResolver;
import org.keycloak.common.util.UriUtils;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.http.HttpRequest;
Expand Down Expand Up @@ -347,7 +348,9 @@ public Response getMainPage() throws IOException, FreeMarkerException {

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

map.put("isSecureContext", isSecureContext);
map.put("serverBaseUrl", serverBaseUrl);
map.put("adminBaseUrl", adminBaseUrl);
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
Expand Down
1 change: 1 addition & 0 deletions themes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build:clean": "shx rm -rf vendor"
},
"dependencies": {
"@noble/hashes": "^1.5.0",
"@patternfly-v5/patternfly": "npm:@patternfly/patternfly@^5.3.1",
"@patternfly/patternfly": "^4.224.5",
"patternfly": "^3.59.5",
Expand Down
8 changes: 8 additions & 0 deletions themes/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ export default defineConfig([
external: ["react"],
plugins,
},
{
input: "src/main/js/web-crypto-shim.js",
output: {
dir: path.join(targetDir, "web-crypto-shim"),
format: "es",
},
plugins,
},
]);
34 changes: 34 additions & 0 deletions themes/src/main/js/web-crypto-shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { sha256 } from '@noble/hashes/sha256';

// Shim for Web Crypto API specifically for Keycloak JS, as this API can sometimes be missing, for example in an insecure context:
// https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
// Since we have decided to support insecure contexts, we (sadly) need to provide a fallback for the Web Crypto API.
if (typeof crypto === "undefined") {
globalThis.crypto = {};
}

if (typeof crypto.subtle === "undefined") {
Object.defineProperty(crypto, "subtle", {
value: {
digest: async (algorithm, data) => {
if (algorithm === "SHA-256") {
return sha256(data);
}

throw new Error("Unsupported algorithm");
}
}
});
}

if (typeof crypto.getRandomValues === "undefined") {
Object.defineProperty(crypto, "getRandomValues", {
value: (array) => {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}

return array;
}
});
}