diff --git a/README.md b/README.md index 74ae96a..0ba8120 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Parameter | Description | Default `minTimeBetweenJwksRequests` | Amount of time, in seconds, specifying minimum interval between two requests to Keycloak to retrieve new public keys. | `10` `loginUrl` | An URL of the endpoint responsible for obtaining OAuth2.0's Authorization Code grant. It is exposed only if `bearerOnly` is set to false. | `/sso/login` `logoutUrl` | An URL the endpoint responsible for handling logout procedure. It is exposed only if `bearerOnly` is set to false. | `/sso/logout` +`apilogoutUrl` | An URL the endpoint responsible for handling logout procedure via a non-browser invocation (without redirects). | `/api/logout` `principalUrl` | An URL of the endpoint exposing resource owner's data (such as its name, ID token, access token etc.). Use `null` in order not to expose this endpoint at all. | `/api/principal` `principalConversion` | A function which alters principal representation exposed by `principalUrl` endpoint before it's sent in a response. Define this function if you don't want for example an access token to be exposed. | `undefined` (no conversion) `principalNameAttribute` | An access/ID token attribute which will be used as the principal name (user name). It will fallback to *sub* token attribute in case the *principalNameAttribute* is not present. Possible values are *sub*, *preferred_username*, *email*, *name*. | `name` diff --git a/changelog.md b/changelog.md index 9852f9d..9143ce5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Add api logout endpoint for non-browser invocation (without redirection) + ## 1.2.2 * Pass locale query (kc_locale or ui_locale) in logout redirect uri to login page diff --git a/package.json b/package.json index 666f8ed..f120df8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keycloak-hapi", - "version": "2.0.0", + "version": "2.1.0", "description": "Integration of Keycloak Authorization Server with HapiJS", "main": "dist/index.js", "scripts": { @@ -35,7 +35,8 @@ }, "peerDependencies": { "hapi": "^17.0.0", - "yar": "^9.1.0" + "yar": "^9.1.0", + "wreck": "^14.2.0" }, "engines": { "node": ">=8", diff --git a/src/index.js b/src/index.js index 8a869ee..a4cabbf 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,8 @@ const Token = require('keycloak-connect/middleware/auth-utils/token'); const Grant = require('keycloak-connect/middleware/auth-utils/grant'); const UUID = require('keycloak-connect/uuid'); const Boom = require('boom'); +const Wreck = require('wreck'); +const querystring = require('querystring'); const _ = require('lodash'); const pkg = require('../package.json'); const crypto = require('crypto'); @@ -232,6 +234,7 @@ class KeycloakAdapter { this.config = Object.assign({ loginUrl: '/sso/login', logoutUrl: '/sso/logout', + apiLogout: '/api/logout', principalUrl: '/api/principal', corsOrigin: ['*'], principalConversion: defaultPrincipalConversion, @@ -301,6 +304,11 @@ class KeycloakAdapter { return urljoin(base, this.server.realm.modifiers.route.prefix || ''); } + getClientBasicAuth() { + const clientCredentials = `${this.config.clientId}:${this.config.clientSecret}`; + return Buffer.from(clientCredentials).toString('base64'); + } + getLoginRedirectUrl(request) { return urljoin(this.getBaseUrl(request), this.config.loginUrl, '?auth_callback=1'); } @@ -405,6 +413,7 @@ class KeycloakAdapter { if (!this.config.bearerOnly) { registerLoginRoute(this); registerLogoutRoute(this); + registerApiLogoutRoute(this); registerBackChannelLogoutRoute(this); } if (this.config.principalUrl) { @@ -521,7 +530,7 @@ const registerLogoutRoute = (keycloak) => { path: keycloak.config.logoutUrl, method: 'GET', handler(request, reply) { - keycloak.server.log(['debug', 'keycloak'], 'Signing out'); + keycloak.server.log(['debug', 'keycloak'], 'Signing out using redirection'); const grantStore = keycloak.getGrantStoreByName('session'); grantStore.clearGrant(request); const locale = getLocale(request); @@ -538,6 +547,38 @@ const registerLogoutRoute = (keycloak) => { }); }; +const registerApiLogoutRoute = (keycloak) => { + keycloak.server.route({ + path: keycloak.config.apiLogoutUrl, + method: 'POST', + handler: async (request) => { + keycloak.server.log(['debug', 'keycloak'], 'Signing out using refresh token'); + const grantStore = keycloak.getGrantStoreFor(request); + const grant = grantStore.getGrant(request); + const logoutUrl = keycloak.getLogoutUrl({}); + await Wreck.post(logoutUrl, { + payload: querystring.stringify({ + 'refresh_token': grant.refresh_token.token, + 'client_id': keycloak.config.clientId + }), + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'authorization': `Basic ${keycloak.getClientBasicAuth()}` + } + }); + grantStore.clearGrant(request); + return { + message: 'User has been successfully logged out.' + } + }, + config: { + cors: { + origin: keycloak.config.corsOrigin + } + } + }); +}; + /* This is a plugin registration backward-compatible with Hapijs v14+ */ const register = (server, options, next) => { const adapter = new KeycloakAdapter(server, options);