diff --git a/docs/release-notes/26_3_0.adoc b/docs/release-notes/26_3_0.adoc index 5696378..9ab72fc 100644 --- a/docs/release-notes/26_3_0.adoc +++ b/docs/release-notes/26_3_0.adoc @@ -1,4 +1,4 @@ -This release of Keycloak JS deprecates the built-in Cordova adapters ahead of their removal in the next major version. +This release of Keycloak JS deprecates the built-in Cordova adapters and the use of unbound methods ahead of their removal in the next major version. == Deprecations @@ -19,3 +19,19 @@ This decision was made after extensive discussion with the community (https://gi ==== Migration If you are using Cordova or a similar hybrid app framework, use a <> to provide your own implementation. For users migrating to Capacitor, see https://github.com/keycloak/keycloak-js/issues/27[keycloak/keycloak-js#27] for an ongoing discussion on improving the custom adapter interface to better support this use case. + +=== Calling methods without a bound `this` is deprecated + +Destructuring methods from a `Keycloak` instance (e.g. `const { login } = keycloak`) and calling them without their original `this` context is now deprecated. A deprecation warning will be emitted in the browser console the first time an unbound method is called. In the next major version, this pattern will stop working entirely. + +Instead, always call methods directly on the instance: + +[source,javascript] +---- +// Deprecated: +const { login, logout } = keycloak; +login(); + +// Recommended: +keycloak.login(); +---- diff --git a/lib/deprecations.js b/lib/deprecations.js index 1892f2e..6f69d39 100644 --- a/lib/deprecations.js +++ b/lib/deprecations.js @@ -14,3 +14,25 @@ export function logDeprecation (code, message) { emitted.add(code) console.warn(`[KEYCLOAK] ${code}: ${message}`) } + +/** + * Replaces each listed method on the instance with a bound wrapper that + * emits a deprecation warning when called with an unbound `this`. + * @param {object} instance + * @param {string} code + * @param {string[]} methods + */ +export function deprecatedBoundMethods (instance, code, methods) { + for (const name of methods) { + const original = instance[name] + instance[name] = function (...args) { + if (this !== instance) { + logDeprecation( + `${code}:${name}`, + `Calling '${name}()' without a bound 'this' is deprecated and will stop working in a future major version.` + ) + } + return original.apply(instance, args) + } + } +} diff --git a/lib/keycloak.js b/lib/keycloak.js index 7afa0be..c290b20 100755 --- a/lib/keycloak.js +++ b/lib/keycloak.js @@ -19,7 +19,7 @@ * limitations under the License. */ -import { logDeprecation } from './deprecations.js' +import { deprecatedBoundMethods, logDeprecation } from './deprecations.js' const CONTENT_TYPE_JSON = 'application/json' @@ -168,13 +168,20 @@ export default class Keycloak { } this.#config = config + + deprecatedBoundMethods(this, 'KC-DEP-003', [ + 'init', 'login', 'createLoginUrl', 'logout', 'createLogoutUrl', 'register', + 'createRegisterUrl', 'createAccountUrl', 'accountManagement', 'hasRealmRole', + 'hasResourceRole', 'loadUserProfile', 'loadUserInfo', 'isTokenExpired', + 'updateToken', 'clearToken' + ]) } /** * @param {KeycloakInitOptions} initOptions * @returns {Promise} */ - init = async (initOptions = {}) => { + async init (initOptions = {}) { if (this.didInitialize) { throw new Error("A 'Keycloak' instance can only be initialized once.") } @@ -1196,7 +1203,7 @@ export default class Keycloak { * @param {KeycloakLoginOptions} [options] * @returns {Promise} */ - login = (options) => { + login (options) { return this.#adapter.login(options) } @@ -1204,7 +1211,7 @@ export default class Keycloak { * @param {KeycloakLoginOptions} [options] * @returns {Promise} */ - createLoginUrl = async (options) => { + async createLoginUrl (options) { const state = createUUID() const nonce = createUUID() const redirectUri = this.#adapter.redirectUri(options) @@ -1302,7 +1309,7 @@ export default class Keycloak { * @param {KeycloakLogoutOptions} [options] * @returns {Promise} */ - logout = (options) => { + logout (options) { return this.#adapter.logout(options) } @@ -1310,7 +1317,7 @@ export default class Keycloak { * @param {KeycloakLogoutOptions} [options] * @returns {string} */ - createLogoutUrl = (options) => { + createLogoutUrl (options) { const logoutMethod = options?.logoutMethod ?? this.logoutMethod const url = this.endpoints.logout() @@ -1334,7 +1341,7 @@ export default class Keycloak { * @param {KeycloakRegisterOptions} [options] * @returns {Promise} */ - register = (options) => { + register (options) { return this.#adapter.register(options) } @@ -1342,7 +1349,7 @@ export default class Keycloak { * @param {KeycloakRegisterOptions} [options] * @returns {Promise} */ - createRegisterUrl = (options) => { + createRegisterUrl (options) { return this.createLoginUrl({ ...options, action: 'register' }) } @@ -1350,7 +1357,7 @@ export default class Keycloak { * @param {KeycloakAccountOptions} [options] * @returns {string} */ - createAccountUrl = (options) => { + createAccountUrl (options) { const url = this.#getRealmUrl() if (!url) { @@ -1368,7 +1375,7 @@ export default class Keycloak { /** * @returns {Promise} */ - accountManagement = () => { + accountManagement () { return this.#adapter.accountManagement() } @@ -1376,7 +1383,7 @@ export default class Keycloak { * @param {string} role * @returns {boolean} */ - hasRealmRole = (role) => { + hasRealmRole (role) { const access = this.realmAccess return !!access && access.roles.indexOf(role) >= 0 } @@ -1386,7 +1393,7 @@ export default class Keycloak { * @param {string} [resource] * @returns {boolean} */ - hasResourceRole = (role, resource) => { + hasResourceRole (role, resource) { if (!this.resourceAccess) { return false } @@ -1398,7 +1405,7 @@ export default class Keycloak { /** * @returns {Promise} */ - loadUserProfile = async () => { + async loadUserProfile () { const realmUrl = this.#getRealmUrl() if (!realmUrl) { @@ -1417,7 +1424,7 @@ export default class Keycloak { /** * @returns {Promise<{}>} */ - loadUserInfo = async () => { + async loadUserInfo () { const url = this.endpoints.userinfo() /** @type {{}} */ const userInfo = await fetchJSON(url, { @@ -1431,7 +1438,7 @@ export default class Keycloak { * @param {number} [minValidity] * @returns {boolean} */ - isTokenExpired = (minValidity) => { + isTokenExpired (minValidity) { if (!this.tokenParsed || (!this.refreshToken && this.flow !== 'implicit')) { throw new Error('Not authenticated') } @@ -1459,7 +1466,7 @@ export default class Keycloak { * @param {number} minValidity * @returns {Promise} */ - updateToken = async (minValidity) => { + async updateToken (minValidity) { if (!this.refreshToken) { throw new Error('Unable to update token, no refresh token available.') } @@ -1522,7 +1529,7 @@ export default class Keycloak { return await promise } - clearToken = () => { + clearToken () { if (this.token) { this.#setToken() this.onAuthLogout?.()