Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
User sessions/consents/logout and realm-events (#20)
Browse files Browse the repository at this point in the history
* UserSessionRepresentation interface added
* ListSessions associated with the user
* Logout user from all sessions
* List offline sessions
* Find events on realm
  • Loading branch information
gerdsander authored and zagaria committed Oct 10, 2019
1 parent e3b717d commit c04fb02
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 4 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"prettier.tslintIntegration": true,
"editor.formatOnSave": true,
"javascript.format.enable": false
"javascript.format.enable": false,
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
}
13 changes: 13 additions & 0 deletions src/defs/eventRepresentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import EventType from './eventTypes';

export default interface EventRepresentation {
clientId?: string;
details?: Record<string, any>;
error?: string;
ipAddress?: string;
realmId?: string;
sessionId?: string;
time?: number;
type?: EventType;
userId?: string;
}
108 changes: 108 additions & 0 deletions src/defs/eventTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
enum EventType {

LOGIN = 'LOGIN',
LOGIN_ERROR = 'LOGIN_ERROR',
REGISTER = 'REGISTER',
REGISTER_ERROR = 'REGISTER_ERROR',
LOGOUT = 'LOGOUT',
LOGOUT_ERROR = 'LOGOUT_ERROR',

CODE_TO_TOKEN = 'CODE_TO_TOKEN',
CODE_TO_TOKEN_ERROR = 'CODE_TO_TOKEN_ERROR',

CLIENT_LOGIN = 'CLIENT_LOGIN',
CLIENT_LOGIN_ERROR = 'CLIENT_LOGIN_ERROR',

REFRESH_TOKEN = 'REFRESH_TOKEN',
REFRESH_TOKEN_ERROR = 'REFRESH_TOKEN_ERROR',

VALIDATE_ACCESS_TOKEN = 'VALIDATE_ACCESS_TOKEN',

VALIDATE_ACCESS_TOKEN_ERROR = 'VALIDATE_ACCESS_TOKEN_ERROR',
INTROSPECT_TOKEN = 'INTROSPECT_TOKEN',
INTROSPECT_TOKEN_ERROR = 'INTROSPECT_TOKEN_ERROR',

FEDERATED_IDENTITY_LINK = 'FEDERATED_IDENTITY_LINK',
FEDERATED_IDENTITY_LINK_ERROR = 'FEDERATED_IDENTITY_LINK_ERROR',
REMOVE_FEDERATED_IDENTITY = 'REMOVE_FEDERATED_IDENTITY',
REMOVE_FEDERATED_IDENTITY_ERROR = 'REMOVE_FEDERATED_IDENTITY_ERROR',

UPDATE_EMAIL = 'UPDATE_EMAIL',
UPDATE_EMAIL_ERROR = 'UPDATE_EMAIL_ERROR',
UPDATE_PROFILE = 'UPDATE_PROFILE',
UPDATE_PROFILE_ERROR = 'UPDATE_PROFILE_ERROR',
UPDATE_PASSWORD = 'UPDATE_PASSWORD',
UPDATE_PASSWORD_ERROR = 'UPDATE_PASSWORD_ERROR',
UPDATE_TOTP = 'UPDATE_TOTP',
UPDATE_TOTP_ERROR = 'UPDATE_TOTP_ERROR',
VERIFY_EMAIL = 'VERIFY_EMAIL',
VERIFY_EMAIL_ERROR = 'VERIFY_EMAIL_ERROR',

REMOVE_TOTP = 'REMOVE_TOTP',
REMOVE_TOTP_ERROR = 'REMOVE_TOTP_ERROR',

REVOKE_GRANT = 'REVOKE_GRANT',
REVOKE_GRANT_ERROR = 'REVOKE_GRANT_ERROR',

SEND_VERIFY_EMAIL = 'SEND_VERIFY_EMAIL',
SEND_VERIFY_EMAIL_ERROR = 'SEND_VERIFY_EMAIL_ERROR',
SEND_RESET_PASSWORD = 'SEND_RESET_PASSWORD',
SEND_RESET_PASSWORD_ERROR = 'SEND_RESET_PASSWORD_ERROR',
SEND_IDENTITY_PROVIDER_LINK = 'SEND_IDENTITY_PROVIDER_LINK',
SEND_IDENTITY_PROVIDER_LINK_ERROR = 'SEND_IDENTITY_PROVIDER_LINK_ERROR',
RESET_PASSWORD = 'RESET_PASSWORD',
RESET_PASSWORD_ERROR = 'RESET_PASSWORD_ERROR',

RESTART_AUTHENTICATION = 'RESTART_AUTHENTICATION',
RESTART_AUTHENTICATION_ERROR = 'RESTART_AUTHENTICATION_ERROR',

INVALID_SIGNATURE = 'INVALID_SIGNATURE',
INVALID_SIGNATURE_ERROR = 'INVALID_SIGNATURE_ERROR',
REGISTER_NODE = 'REGISTER_NODE',
REGISTER_NODE_ERROR = 'REGISTER_NODE_ERROR',
UNREGISTER_NODE = 'UNREGISTER_NODE',
UNREGISTER_NODE_ERROR = 'UNREGISTER_NODE_ERROR',

USER_INFO_REQUEST = 'USER_INFO_REQUEST',
USER_INFO_REQUEST_ERROR = 'USER_INFO_REQUEST_ERROR',

IDENTITY_PROVIDER_LINK_ACCOUNT = 'IDENTITY_PROVIDER_LINK_ACCOUNT',
IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR = 'IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR',
IDENTITY_PROVIDER_LOGIN = 'IDENTITY_PROVIDER_LOGIN',
IDENTITY_PROVIDER_LOGIN_ERROR = 'IDENTITY_PROVIDER_LOGIN_ERROR',
IDENTITY_PROVIDER_FIRST_LOGIN = 'IDENTITY_PROVIDER_FIRST_LOGIN',
IDENTITY_PROVIDER_FIRST_LOGIN_ERROR = 'IDENTITY_PROVIDER_FIRST_LOGIN_ERROR',
IDENTITY_PROVIDER_POST_LOGIN = 'IDENTITY_PROVIDER_POST_LOGIN',
IDENTITY_PROVIDER_POST_LOGIN_ERROR = 'IDENTITY_PROVIDER_POST_LOGIN_ERROR',
IDENTITY_PROVIDER_RESPONSE = 'IDENTITY_PROVIDER_RESPONSE',
IDENTITY_PROVIDER_RESPONSE_ERROR = 'IDENTITY_PROVIDER_RESPONSE_ERROR',
IDENTITY_PROVIDER_RETRIEVE_TOKEN = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN',
IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR',
IMPERSONATE = 'IMPERSONATE',
IMPERSONATE_ERROR = 'IMPERSONATE_ERROR',
CUSTOM_REQUIRED_ACTION = 'CUSTOM_REQUIRED_ACTION',
CUSTOM_REQUIRED_ACTION_ERROR = 'CUSTOM_REQUIRED_ACTION_ERROR',
EXECUTE_ACTIONS = 'EXECUTE_ACTIONS',
EXECUTE_ACTIONS_ERROR = 'EXECUTE_ACTIONS_ERROR',
EXECUTE_ACTION_TOKEN = 'EXECUTE_ACTION_TOKEN',
EXECUTE_ACTION_TOKEN_ERROR = 'EXECUTE_ACTION_TOKEN_ERROR',

CLIENT_INFO = 'CLIENT_INFO',
CLIENT_INFO_ERROR = 'CLIENT_INFO_ERROR',
CLIENT_REGISTER = 'CLIENT_REGISTER',
CLIENT_REGISTER_ERROR = 'CLIENT_REGISTER_ERROR',
CLIENT_UPDATE = 'CLIENT_UPDATE',
CLIENT_UPDATE_ERROR = 'CLIENT_UPDATE_ERROR',
CLIENT_DELETE = 'CLIENT_DELETE',
CLIENT_DELETE_ERROR = 'CLIENT_DELETE_ERROR',

CLIENT_INITIATED_ACCOUNT_LINKING = 'CLIENT_INITIATED_ACCOUNT_LINKING',
CLIENT_INITIATED_ACCOUNT_LINKING_ERROR = 'CLIENT_INITIATED_ACCOUNT_LINKING_ERROR',
TOKEN_EXCHANGE = 'TOKEN_EXCHANGE',
TOKEN_EXCHANGE_ERROR = 'TOKEN_EXCHANGE_ERROR',

PERMISSION_TOKEN = 'PERMISSION_TOKEN',
PERMISSION_TOKEN_ERROR = 'PERMISSION_TOKEN_ERROR',
}

export default EventType;
9 changes: 9 additions & 0 deletions src/defs/userSessionRepresentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default interface UserSessionRepresentation {
id?: string;
clients?: Record<string, string>;
ipAddress?: string;
lastAccess?: number;
start?: number;
userId?: string;
username?: string;
}
18 changes: 18 additions & 0 deletions src/resources/realms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Resource from './resource';
import RealmRepresentation from '../defs/realmRepresentation';
import EventRepresentation from '../defs/eventRepresentation';
import EventType from '../defs/eventTypes';

import {KeycloakAdminClient} from '../client';

export class Realms extends Resource {
Expand Down Expand Up @@ -40,6 +43,21 @@ export class Realms extends Resource {
urlParamKeys: ['realm'],
});

/**
* Get events Returns all events, or filters them based on URL query parameters listed here
*/
public findEvents = this.makeRequest<{
realm: string,
client?: string, dateFrom?: Date, dateTo?: Date,
first?: number, ipAddress?: string, max?: number,
type?: EventType, user?: string,
}, EventRepresentation[]>({
method: 'GET',
path: '/{realm}/events',
urlParamKeys: ['realm'],
queryParamKeys: ['client', 'dateFrom', 'dateTo', 'first', 'ipAddress', 'max', 'type', 'user'],
});

constructor(client: KeycloakAdminClient) {
super(client, {
path: '/admin/realms',
Expand Down
62 changes: 62 additions & 0 deletions src/resources/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Resource from './resource';
import UserRepresentation from '../defs/userRepresentation';
import UserConsentRepresentation from '../defs/userConsentRepresentation';
import UserSessionRepresentation from '../defs/userSessionRepresentation';
import {KeycloakAdminClient} from '../client';
import MappingsRepresentation from '../defs/mappingsRepresentation';
import RoleRepresentation, {
Expand Down Expand Up @@ -290,6 +292,66 @@ export class Users extends Resource<{realm?: string}> {
},
});

/**
* list user sessions
*/
public listSessions = this.makeRequest<
{id: string},
UserSessionRepresentation[]
>({
method: 'GET',
path: '/{id}/sessions',
urlParamKeys: ['id'],
});

/**
* list offline sessions associated with the user and client
*/
public listOfflineSessions = this.makeRequest<
{id: string, clientId: string},
UserSessionRepresentation[]
>({
method: 'GET',
path: '/{id}/offline-sessions/{clientId}',
urlParamKeys: ['id', 'clientId'],
});

/**
* logout user from all sessions
*/
public logout = this.makeRequest<
{id: string},
void
>({
method: 'POST',
path: '/{id}/logout',
urlParamKeys: ['id'],
});

/**
* list consents granted by the user
*/
public listConsents = this.makeRequest<
{id: string},
UserConsentRepresentation[]
>({
method: 'GET',
path: '/{id}/consents',
urlParamKeys: ['id'],
});

/**
* revoke consent and offline tokens for particular client from user
*/
public revokeConsent = this.makeRequest<
{id: string, clientId: string},
void
>({
method: 'DELETE',
path: '/{id}/consents/{clientId}',
urlParamKeys: ['id', 'clientId'],
});

constructor(client: KeycloakAdminClient) {
super(client, {
path: '/admin/realms/{realm}/users',
Expand Down
34 changes: 33 additions & 1 deletion test/realms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ declare module 'mocha' {
}
}

describe('Realms', function() {
describe('Realms', function () {
before(async () => {
this.kcAdminClient = new KeycloakAdminClient();
await this.kcAdminClient.auth(credentials);
Expand Down Expand Up @@ -71,4 +71,36 @@ describe('Realms', function() {
});
expect(realm).to.be.null;
});

describe('Realm Events', function () {
before(async () => {
this.kcAdminClient = new KeycloakAdminClient();
await this.kcAdminClient.auth(credentials);

const realmId = faker.internet.userName().toLowerCase();
const realmName = faker.internet.userName().toLowerCase();
const realm = await this.kcAdminClient.realms.create({
id: realmId,
realm: realmName,
});
expect(realm.realmName).to.be.equal(realmName);
this.currentRealmId = realmId;
this.currentRealmName = realmName;
});

it('list events of a realm', async () => {
// @TODO: In order to test it, there have to be events
const events = await this.kcAdminClient.realms.findEvents({realm: this.currentRealmName});

expect(events).to.be.ok;
});

after(async () => {
await this.kcAdminClient.realms.del({realm: this.currentRealmName});
const realm = await this.kcAdminClient.realms.findOne({
realm: this.currentRealmName,
});
expect(realm).to.be.null;
});
});
});
84 changes: 82 additions & 2 deletions test/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {KeycloakAdminClient} from '../src/client';
import {credentials} from './constants';
import faker from 'faker';
import UserRepresentation from '../src/defs/userRepresentation';
import UserSessionRepresentation from '../src/defs/userSessionRepresentation';
import RoleRepresentation from '../src/defs/roleRepresentation';
import ClientRepresentation from '../src/defs/clientRepresentation';
import {RequiredActionAlias} from '../src/defs/requiredActionProviderRepresentation';
Expand All @@ -23,7 +24,7 @@ declare module 'mocha' {
}
}

describe('Users', function() {
describe('Users', function () {
this.timeout(10000);

before(async () => {
Expand Down Expand Up @@ -364,7 +365,86 @@ describe('Users', function() {
});
});

describe('Federated Identity user integration', function() {
describe('User sessions', function () {
before(async () => {
this.kcAdminClient = new KeycloakAdminClient();
await this.kcAdminClient.auth(credentials);

// create user
const username = faker.internet.userName();
await this.kcAdminClient.users.create({
username,
email: '[email protected]',
enabled: true,
});
const users = await this.kcAdminClient.users.find({username});
expect(users[0]).to.be.ok;
this.currentUser = users[0];

// create client
const clientId = faker.internet.userName();
await this.kcAdminClient.clients.create({
clientId, consentRequired: true,
});

const clients = await this.kcAdminClient.clients.find({clientId});
expect(clients[0]).to.be.ok;
this.currentClient = clients[0];
});

after(async () => {
await this.kcAdminClient.users.del({
id: this.currentUser.id,
});

await this.kcAdminClient.clients.del({
id: this.currentClient.id,
});
});

it('list user sessions', async () => {
// @TODO: In order to test it, currentUser has to be logged in

const userSessions = await this.kcAdminClient.users.listSessions({id: this.currentUser.id});

expect(userSessions).to.be.ok;
});

it('list users off-line sessions', async () => {
// @TODO: In order to test it, currentUser has to be logged in

const userOfflineSessions = await this.kcAdminClient.users.listOfflineSessions(
{id: this.currentUser.id, clientId: this.currentClient.id},
);

expect(userOfflineSessions).to.be.ok;
});

it('logout user from all sessions', async () => {
// @TODO: In order to test it, currentUser has to be logged in

await this.kcAdminClient.users.logout({id: this.currentUser.id});
});

it('list consents granted by the user', async () => {
const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id});

expect(consents).to.be.ok;
});

it('revoke consent and offline tokens for particular client', async () => {
// @TODO: In order to test it, currentUser has to granted consent to client
const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id});

if (consents.length) {
const consent = consents[0];

await this.kcAdminClient.users.revokeConsent({id: this.currentUser.id, clientId: consent.clientId});
}
});
});

describe('Federated Identity user integration', function () {
before(async () => {
this.kcAdminClient = new KeycloakAdminClient();
await this.kcAdminClient.auth(credentials);
Expand Down

0 comments on commit c04fb02

Please sign in to comment.