Compute SHA-256 digest for PKCE using the Web Crypto API (#33251)
Closes #33250 Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
6424708695
commit
021a2af2fd
9 changed files with 63 additions and 45 deletions
1
.github/actions/conditional/conditions
vendored
1
.github/actions/conditional/conditions
vendored
|
@ -43,6 +43,7 @@ rest/admin-ui-ext/ js
|
||||||
services/ js
|
services/ js
|
||||||
js/apps/account-ui/ ci ci-webauthn
|
js/apps/account-ui/ ci ci-webauthn
|
||||||
js/libs/ui-shared/ ci ci-webauthn
|
js/libs/ui-shared/ ci ci-webauthn
|
||||||
|
js/libs/keycloak-js/ ci ci-quarkus
|
||||||
|
|
||||||
# The sections below contain a sub-set of files existing in the project which are supported languages by CodeQL.
|
# The sections below contain a sub-set of files existing in the project which are supported languages by CodeQL.
|
||||||
# See: https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/
|
# See: https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/
|
||||||
|
|
|
@ -22,7 +22,7 @@ If you are using the JavaScript adapter, you can also achieve the same behavior
|
||||||
----
|
----
|
||||||
const keycloak = new Keycloak('keycloak.json');
|
const keycloak = new Keycloak('keycloak.json');
|
||||||
|
|
||||||
keycloak.createLoginUrl({
|
await keycloak.createLoginUrl({
|
||||||
idpHint: 'facebook'
|
idpHint: 'facebook'
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
|
|
@ -303,3 +303,13 @@ The following event types are now deprecated and will be removed in a future ver
|
||||||
= `setOrCreateChild()` method removed from JavaScript Admin Client
|
= `setOrCreateChild()` method removed from JavaScript Admin Client
|
||||||
|
|
||||||
The `groups.setOrCreateChild()` method has been removed from that JavaScript-based Admin Client. If you are still using this method then use the `createChildGroup()` or `updateChildGroup()` methods instead.
|
The `groups.setOrCreateChild()` method has been removed from that JavaScript-based Admin Client. If you are still using this method then use the `createChildGroup()` or `updateChildGroup()` methods instead.
|
||||||
|
|
||||||
|
= Keycloak JS methods for login are now `async`
|
||||||
|
|
||||||
|
Keycloak JS now utilizes the Web Crypto API to calculate the SHA-256 digests needed to support PKCE. Due to the asynchronous nature of this API the following public methods now return a `Promise`:
|
||||||
|
|
||||||
|
- `login()`
|
||||||
|
- `createLoginUrl()`
|
||||||
|
- `createRegisterUrl()`
|
||||||
|
|
||||||
|
Make sure to update your code to `await` these methods.
|
||||||
|
|
|
@ -64,7 +64,7 @@ To enable the _silent_ `check-sso`, you provide a `silentCheckSsoRedirectUri` at
|
||||||
|
|
||||||
[source,javascript]
|
[source,javascript]
|
||||||
----
|
----
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
onLoad: 'check-sso',
|
onLoad: 'check-sso',
|
||||||
silentCheckSsoRedirectUri: `${r"${location.origin}"}/silent-check-sso.html`
|
silentCheckSsoRedirectUri: `${r"${location.origin}"}/silent-check-sso.html`
|
||||||
});
|
});
|
||||||
|
@ -93,7 +93,7 @@ To enable `login-required` set `onLoad` to `login-required` and pass to the init
|
||||||
|
|
||||||
[source,javascript]
|
[source,javascript]
|
||||||
----
|
----
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
onLoad: 'login-required'
|
onLoad: 'login-required'
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
@ -158,7 +158,7 @@ To enable implicit flow, you enable the *Implicit Flow Enabled* flag for the cli
|
||||||
|
|
||||||
[source,javascript]
|
[source,javascript]
|
||||||
----
|
----
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
flow: 'implicit'
|
flow: 'implicit'
|
||||||
})
|
})
|
||||||
----
|
----
|
||||||
|
@ -176,7 +176,7 @@ For the Hybrid flow, you need to pass the parameter `flow` with value `hybrid` t
|
||||||
|
|
||||||
[source,javascript]
|
[source,javascript]
|
||||||
----
|
----
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
flow: 'hybrid'
|
flow: 'hybrid'
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
@ -200,7 +200,7 @@ You can activate the native mode by passing the adapter type `cordova-native` to
|
||||||
|
|
||||||
[source,javascript]
|
[source,javascript]
|
||||||
----
|
----
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
adapter: 'cordova-native'
|
adapter: 'cordova-native'
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
@ -242,7 +242,7 @@ import KeycloakCapacitorAdapter from 'keycloak-capacitor-adapter';
|
||||||
|
|
||||||
const keycloak = new Keycloak();
|
const keycloak = new Keycloak();
|
||||||
|
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
adapter: KeycloakCapacitorAdapter,
|
adapter: KeycloakCapacitorAdapter,
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
@ -257,7 +257,7 @@ import Keycloak, { KeycloakAdapter } from 'keycloak-js';
|
||||||
|
|
||||||
// Implement the 'KeycloakAdapter' interface so that all required methods are guaranteed to be present.
|
// Implement the 'KeycloakAdapter' interface so that all required methods are guaranteed to be present.
|
||||||
const MyCustomAdapter: KeycloakAdapter = {
|
const MyCustomAdapter: KeycloakAdapter = {
|
||||||
login(options) {
|
async login(options) {
|
||||||
// Write your own implementation here.
|
// Write your own implementation here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +266,7 @@ const MyCustomAdapter: KeycloakAdapter = {
|
||||||
|
|
||||||
const keycloak = new Keycloak();
|
const keycloak = new Keycloak();
|
||||||
|
|
||||||
keycloak.init({
|
await keycloak.init({
|
||||||
adapter: MyCustomAdapter,
|
adapter: MyCustomAdapter,
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
@ -391,7 +391,7 @@ Returns a promise that resolves when initialization completes.
|
||||||
|
|
||||||
*login(options)*
|
*login(options)*
|
||||||
|
|
||||||
Redirects to login form.
|
Redirects to login form, returns a Promise.
|
||||||
|
|
||||||
Options is an optional Object, where:
|
Options is an optional Object, where:
|
||||||
|
|
||||||
|
@ -417,7 +417,7 @@ See link:{adminguide_link}#con-aia_server_administration_guide[Application Initi
|
||||||
|
|
||||||
*createLoginUrl(options)*
|
*createLoginUrl(options)*
|
||||||
|
|
||||||
Returns the URL to login form.
|
Returns a Promise containing the URL to login form.
|
||||||
|
|
||||||
Options is an optional Object, which supports same options as the function `login` .
|
Options is an optional Object, which supports same options as the function `login` .
|
||||||
|
|
||||||
|
@ -445,7 +445,7 @@ Options are same as for the login method but 'action' is set to 'register'
|
||||||
|
|
||||||
*createRegisterUrl(options)*
|
*createRegisterUrl(options)*
|
||||||
|
|
||||||
Returns the url to registration page. Shortcut for createLoginUrl with option action = 'register'
|
Returns a Promise containing the url to registration page. Shortcut for createLoginUrl with option action = 'register'
|
||||||
|
|
||||||
Options are same as for the createLoginUrl method but 'action' is set to 'register'
|
Options are same as for the createLoginUrl method but 'action' is set to 'register'
|
||||||
|
|
||||||
|
|
4
js/libs/keycloak-js/dist/keycloak.d.ts
vendored
4
js/libs/keycloak-js/dist/keycloak.d.ts
vendored
|
@ -575,7 +575,7 @@ declare class Keycloak {
|
||||||
* Returns the URL to login form.
|
* Returns the URL to login form.
|
||||||
* @param options Supports same options as Keycloak#login.
|
* @param options Supports same options as Keycloak#login.
|
||||||
*/
|
*/
|
||||||
createLoginUrl(options?: KeycloakLoginOptions): string;
|
createLoginUrl(options?: KeycloakLoginOptions): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to logout the user.
|
* Returns the URL to logout the user.
|
||||||
|
@ -587,7 +587,7 @@ declare class Keycloak {
|
||||||
* Returns the URL to registration page.
|
* Returns the URL to registration page.
|
||||||
* @param options The options used for creating the registration URL.
|
* @param options The options used for creating the registration URL.
|
||||||
*/
|
*/
|
||||||
createRegisterUrl(options?: KeycloakRegisterOptions): string;
|
createRegisterUrl(options?: KeycloakRegisterOptions): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to the Account Management Console.
|
* Returns the URL to the Account Management Console.
|
||||||
|
|
|
@ -78,7 +78,6 @@
|
||||||
"shx": "^0.3.4"
|
"shx": "^0.3.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^1.5.0",
|
|
||||||
"jwt-decode": "^4.0.0"
|
"jwt-decode": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ function defineOptions({
|
||||||
file: path.join(targetDir, `${file}.mjs`),
|
file: path.join(targetDir, `${file}.mjs`),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
external: ["@noble/hashes/sha256", "jwt-decode"],
|
external: ["jwt-decode"],
|
||||||
},
|
},
|
||||||
// Legacy Universal Module Definition, or “UMD”, with inlined dependencies.
|
// Legacy Universal Module Definition, or “UMD”, with inlined dependencies.
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
if (typeof Promise === 'undefined') {
|
if (typeof Promise === 'undefined') {
|
||||||
|
@ -200,9 +199,9 @@ function Keycloak (config) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkSsoSilently = function() {
|
var checkSsoSilently = async function() {
|
||||||
var ifrm = document.createElement("iframe");
|
var ifrm = document.createElement("iframe");
|
||||||
var src = kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
|
var src = await kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
|
||||||
ifrm.setAttribute("src", src);
|
ifrm.setAttribute("src", src);
|
||||||
ifrm.setAttribute("sandbox", "allow-storage-access-by-user-activation allow-scripts allow-same-origin");
|
ifrm.setAttribute("sandbox", "allow-storage-access-by-user-activation allow-scripts allow-same-origin");
|
||||||
ifrm.setAttribute("title", "keycloak-silent-check-sso");
|
ifrm.setAttribute("title", "keycloak-silent-check-sso");
|
||||||
|
@ -371,13 +370,13 @@ function Keycloak (config) {
|
||||||
return String.fromCharCode.apply(null, chars);
|
return String.fromCharCode.apply(null, chars);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePkceChallenge(pkceMethod, codeVerifier) {
|
async function generatePkceChallenge(pkceMethod, codeVerifier) {
|
||||||
if (pkceMethod !== "S256") {
|
if (pkceMethod !== "S256") {
|
||||||
throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`);
|
throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash codeVerifier, then encode as url-safe base64 without padding
|
// hash codeVerifier, then encode as url-safe base64 without padding
|
||||||
const hashBytes = sha256(codeVerifier);
|
const hashBytes = new Uint8Array(await sha256Digest(codeVerifier));
|
||||||
const encodedHash = bytesToBase64(hashBytes)
|
const encodedHash = bytesToBase64(hashBytes)
|
||||||
.replace(/\+/g, '-')
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, '_')
|
.replace(/\//g, '_')
|
||||||
|
@ -395,7 +394,7 @@ function Keycloak (config) {
|
||||||
return JSON.stringify(claims);
|
return JSON.stringify(claims);
|
||||||
}
|
}
|
||||||
|
|
||||||
kc.createLoginUrl = function(options) {
|
kc.createLoginUrl = async function(options) {
|
||||||
var state = createUUID();
|
var state = createUUID();
|
||||||
var nonce = createUUID();
|
var nonce = createUUID();
|
||||||
|
|
||||||
|
@ -473,11 +472,15 @@ function Keycloak (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kc.pkceMethod) {
|
if (kc.pkceMethod) {
|
||||||
var codeVerifier = generateCodeVerifier(96);
|
if (!globalThis.isSecureContext) {
|
||||||
callbackState.pkceCodeVerifier = codeVerifier;
|
logWarn('[KEYCLOAK] PKCE is only supported in secure contexts (HTTPS)');
|
||||||
var pkceChallenge = generatePkceChallenge(kc.pkceMethod, codeVerifier);
|
} else {
|
||||||
url += '&code_challenge=' + pkceChallenge;
|
var codeVerifier = generateCodeVerifier(96);
|
||||||
url += '&code_challenge_method=' + kc.pkceMethod;
|
callbackState.pkceCodeVerifier = codeVerifier;
|
||||||
|
var pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);
|
||||||
|
url += '&code_challenge=' + pkceChallenge;
|
||||||
|
url += '&code_challenge_method=' + kc.pkceMethod;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callbackStorage.add(callbackState);
|
callbackStorage.add(callbackState);
|
||||||
|
@ -511,12 +514,12 @@ function Keycloak (config) {
|
||||||
return adapter.register(options);
|
return adapter.register(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
kc.createRegisterUrl = function(options) {
|
kc.createRegisterUrl = async function(options) {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
options = {};
|
options = {};
|
||||||
}
|
}
|
||||||
options.action = 'register';
|
options.action = 'register';
|
||||||
return kc.createLoginUrl(options);
|
return await kc.createLoginUrl(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
kc.createAccountUrl = function(options) {
|
kc.createAccountUrl = function(options) {
|
||||||
|
@ -1315,8 +1318,8 @@ function Keycloak (config) {
|
||||||
function loadAdapter(type) {
|
function loadAdapter(type) {
|
||||||
if (!type || type == 'default') {
|
if (!type || type == 'default') {
|
||||||
return {
|
return {
|
||||||
login: function(options) {
|
login: async function(options) {
|
||||||
window.location.assign(kc.createLoginUrl(options));
|
window.location.assign(await kc.createLoginUrl(options));
|
||||||
return createPromise().promise;
|
return createPromise().promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1428,11 +1431,11 @@ function Keycloak (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login: function(options) {
|
login: async function(options) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
|
|
||||||
var cordovaOptions = createCordovaOptions(options);
|
var cordovaOptions = createCordovaOptions(options);
|
||||||
var loginUrl = kc.createLoginUrl(options);
|
var loginUrl = await kc.createLoginUrl(options);
|
||||||
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
|
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
|
|
||||||
|
@ -1550,9 +1553,9 @@ function Keycloak (config) {
|
||||||
loginIframe.enable = false;
|
loginIframe.enable = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login: function(options) {
|
login: async function(options) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
var loginUrl = kc.createLoginUrl(options);
|
var loginUrl = await kc.createLoginUrl(options);
|
||||||
|
|
||||||
universalLinks.subscribe('keycloak', function(event) {
|
universalLinks.subscribe('keycloak', function(event) {
|
||||||
universalLinks.unsubscribe('keycloak');
|
universalLinks.unsubscribe('keycloak');
|
||||||
|
@ -1748,8 +1751,22 @@ function Keycloak (config) {
|
||||||
|
|
||||||
export default Keycloak;
|
export default Keycloak;
|
||||||
|
|
||||||
// See: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
/**
|
||||||
|
* @param {ArrayBuffer} bytes
|
||||||
|
* @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
|
*/
|
||||||
function bytesToBase64(bytes) {
|
function bytesToBase64(bytes) {
|
||||||
const binString = String.fromCodePoint(...bytes);
|
const binString = String.fromCodePoint(...bytes);
|
||||||
return btoa(binString);
|
return btoa(binString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example
|
||||||
|
*/
|
||||||
|
async function sha256Digest(message) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(message);
|
||||||
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
|
@ -380,9 +380,6 @@ importers:
|
||||||
|
|
||||||
js/libs/keycloak-js:
|
js/libs/keycloak-js:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes':
|
|
||||||
specifier: ^1.5.0
|
|
||||||
version: 1.5.0
|
|
||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
@ -1096,10 +1093,6 @@ packages:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
'@noble/hashes@1.5.0':
|
|
||||||
resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
|
|
||||||
engines: {node: ^14.21.3 || >=16}
|
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -5678,8 +5671,6 @@ snapshots:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
'@noble/hashes@1.5.0': {}
|
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
|
|
Loading…
Reference in a new issue