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

Ability to set realmName (and maybe other settings) after first authentication #3

Closed
karlhorky opened this issue Sep 26, 2018 · 4 comments

Comments

@karlhorky
Copy link
Contributor

karlhorky commented Sep 26, 2018

Use cases

  1. The Keycloak root admin user wants to log in to the master realm and then do all further operations in a secondary realm-b realm, such as creating a group. It would be desirable to not have to pass realm-b for every call.
  2. The Keycloak root admin user wants to log in to the master realm and do most operations in the master realm, except for a single operation in a realm-c realm. This is apparently already covered by Ability to set realmName (and maybe other settings) after first authentication #3 (comment). I'll document this in the PR.

Prior Art

The keycloak-admin-client allows for use of a different realm via requiring the user to repeat the realm for every call:

// Create the user
return client.users.create(realmName, testUser).then((user) => {
  // ...
});

Proposed API

This is a rough first version of the proposed API. The final version may look different.

const realmNameA = 'master';
const connectionSettings = {
  baseUrl: 'http://keycloak:8080/auth',
  realmName: realmNameA,
};
const keycloakClient = new KeycloakAdminClient(connectionSettings);

// Authenticate with `master` realm
await keycloakClient.auth(userSettings);

// Set defaults for all further requests
const realmNameB = 'realm-b';
keycloakClient.setConfig({
  realmName: realmNameB,
  // ...other options...
});

// One-off operation to create a group on `realm-c`
const realmNameC = 'realm-c';
await keycloakClient.groups.create({name: 'editor'}, {
  realmName: realmNameC,
  // ...other options...
}));

Technical Background

1) First instantiation of Keycloak admin client

The following user code...

const realmNameA = 'master';
const connectionSettings = {
  baseUrl: 'http://keycloak:8080/auth',
  realmName: realmNameA,
};
const keycloakClient = new KeycloakAdminClient(connectionSettings);

...does the following things:

1.1) Sets the realmName property on the instance to original realmName ("realmNameA")
1.2) Passes that instance to the constructor of various resources
1.3) Passes the realmName further to the Resource constructor (via urlParams)
1.4) Passes the realmName further to the Agent constructor (via urlParams)
1.5) Sets the baseParams instance property, which will be used for URL parameters in all further requests
1.6) Further requests pass along the baseParams to the requestWithParams method
1.7) The urlParams are templated
1.8) And passed along to Axios

// Excerpt: src/client.ts
constructor(args?: ClientArgs) {
  // ...
  this.realmName = args && args.realmName || defaultRealm;  // 1.1
  // ...
  this.users = new Users(this);  // 1.2
  // more resources...
  // ...
}
// Excerpt: src/resources/groups.ts
export class Groups extends Resource<{realm?: string}> {
// ...
  constructor(client: KeycloakAdminClient) {
    super(client, {
      path: '/admin/realms/{realm}/groups',
      urlParams: {
        realm: client.realmName  // 1.3
      }
    });
  }
// Excerpt: src/resources/resource.ts
constructor(client: KeycloakAdminClient, settings: {
  path?: string, urlParams?: ParamType
} = {}) {
  this.agent = new Agent({
    client,
    ...settings  // 1.4
  });
}
// Excerpt: src/resources/agent.ts
constructor({
  client, path = '/', urlParams = {}
}: {
  client: KeycloakAdminClient,
  path?: string,
  urlParams?: Record<string, any>
}) {
  this.baseParams = urlParams;  // 1.5
  // ...
}
// ...
public request({
  method,
  path = '',
  urlParams = [],
  querystring = [],
  catchNotFound = false,
  keyTransform,
  payloadKey
}: RequestArgs) {
  return async (payload: any = {}) => {
    const selected = [...Object.keys(this.baseParams), ...urlParams];
    const mergedParams = {...this.baseParams, ...pick(payload, selected)};  // 1.6
    // ...
    return this.requestWithParams({
      // ...
      urlParams: mergedParams,  // 1.6
      // ...
    });
  };
}
// ...
private async requestWithParams(
  {
    method,
    path,
    payload,
    urlParams,
    queryParams,
    catchNotFound,
    payloadKey
  }:
  {
    method: string,
    path: string,
    payload: any,
    urlParams: any,
    queryParams?: Record<string, any> | null,
    catchNotFound: boolean,
    payloadKey?: string
  }) {
  const newPath = join(this.basePath, path);

  // parse
  const temp = template.parse(newPath);
  const parsedPath = temp.expand(urlParams);  // 1.7
  const url = `${this.baseUrl}${parsedPath}`;  // 1.7

  // prepare request configs
  const requestConfig: AxiosRequestConfig = {
    ...this.requestConfigs,
    method,
    url,  // 1.8
    headers: {
      Authorization: `bearer ${this.client.getAccessToken()}`
    }
  };
  // ...
  const res = await axios(requestConfig);
  // ...
}

2) Authentication via call of auth method

The following user code...

await keycloakClient.auth(userSettings);

...does the following things:

2.1) Passes realmNameA to getToken
2.2) Uses this to build the URL
2.3) The URL is passed along to Axios

// Excerpt: src/client.ts
public async auth(credential: Credential) {
  const {accessToken} = await getToken({
    baseUrl: this.baseUrl,
    realmName: this.realmName,  // 2.1
    credential,
    requestConfigs: this.requestConfigs
  });
  // ...
}
// Excerpt: src/utils/auth.ts
export const getToken = async (settings: Settings): Promise<TokenResponse> => {
  // ...
  const realmName = settings.realmName || defaultRealm;
  const url = `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`;  // 2.2
  // ...
  const {data} = await axios.post(url, payload, configs);  // 2.3
  // ...
};

3) Call of resource methods

The following user code...

await keycloakClient.groups.create({name: 'editor'}));

...does the following things:

3.1) Call the agent's request method, using the existing instance properties

// Excerpt: src/resources/groups.ts
public create = this.makeRequest<GroupRepresentation, void>({
  method: 'POST'
});
// Excerpt: src/resources/resource.ts
public makeRequest =
< PayloadType = any,
  ResponseType = any>(args: RequestArgs): (payload?: PayloadType & ParamType) => Promise<ResponseType> => {
return this.agent.request(args);  // 3.1
}
@karlhorky
Copy link
Contributor Author

I will take a shot at implementing this because I need it for amazeeio/lagoon.

@wwwy3y3
Copy link
Contributor

wwwy3y3 commented Sep 28, 2018

Hi @karlhorky
Sorry for the late response.

For use case 2:

The Keycloak root admin user wants to log in to the master realm and do most operations in the master realm, except for a single operation in a realm-c realm.

If you authorize the client with master realm, you can access resources in other realms like this:

await this.kcAdminClient.users.create({
      realm: "another-realm",
      username: "username",
      email: 'wwwy3y3@canner.io'
});

There are some tests in https://github.com/Canner/keycloak-admin/blob/master/test/crossRealm.spec.ts

As for case 1:

This requires some implemetation. I'll look into your technical background & PR later.

@karlhorky
Copy link
Contributor Author

karlhorky commented Sep 28, 2018

If you authorize the client with master realm, you can access resources in other realms like this

Ah ok, great! Then I can just solve this by documenting it. I'll simplify the solution then for case 1 too.

Edit: I've removed the settings object in 789c343 and documented operations in other realms in the readme in 4a51f0d

@karlhorky
Copy link
Contributor Author

I've removed the "Work in Progress" labels from the pull request, ready for review when you have a minute!

@wwwy3y3 wwwy3y3 closed this as completed in #4 Oct 2, 2018
legenddam added a commit to legenddam/nodejs_admim_client that referenced this issue Feb 25, 2020
legenddam added a commit to legenddam/nodejs_admim_client that referenced this issue Feb 25, 2020
Heather0303 pushed a commit to Heather0303/keycloak-nodejs-admin-client that referenced this issue Apr 3, 2020
Heather0303 pushed a commit to Heather0303/keycloak-nodejs-admin-client that referenced this issue Apr 3, 2020
HappyVirgo added a commit to HappyVirgo/nodejs-admin-client that referenced this issue Jul 27, 2021
HappyVirgo added a commit to HappyVirgo/nodejs-admin-client that referenced this issue Jul 27, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants