Skip to content

Commit

Permalink
[authentication-keycloak] Use reactivity-API internally (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbeckem authored Aug 15, 2024
1 parent 58ce24f commit f48bb02
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-mice-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-pioneer/authentication-keycloak": minor
---

Update Keycloak JavaScript adapter to v25.
5 changes: 5 additions & 0 deletions .changeset/three-parents-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-pioneer/authentication-keycloak": patch
---

Refactor: use reactivity API internally to maintain the current state.
22 changes: 12 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 33 additions & 18 deletions src/packages/authentication-keycloak/KeycloakAuthPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { reactive, watch } from "@conterra/reactivity-core";
import {
AuthPlugin,
AuthPluginEvents,
AuthState,
LoginBehavior
} from "@open-pioneer/authentication";
import { EventEmitter, createLogger } from "@open-pioneer/core";
import { EventEmitter, Resource, createLogger, destroyResource } from "@open-pioneer/core";
import { NotificationService } from "@open-pioneer/notifier";
import {
PackageIntl,
Service,
ServiceOptions,
type DECLARE_SERVICE_INTERFACE,
PackageIntl
type DECLARE_SERVICE_INTERFACE
} from "@open-pioneer/runtime";
import Keycloak, {
type KeycloakConfig,
Expand All @@ -34,9 +35,6 @@ export class KeycloakAuthPlugin
{
declare [DECLARE_SERVICE_INTERFACE]: "authentication-keycloak.KeycloakAuthPlugin";

#state: AuthState = {
kind: "pending"
};
#notifier: NotificationService;
#intl: PackageIntl;
#keycloakOptions: KeycloakOptions;
Expand All @@ -45,6 +43,11 @@ export class KeycloakAuthPlugin
#loginOptions: KeycloakLoginOptions;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#timerId: any;
#watcher: Resource | undefined;

#state = reactive<AuthState>({
kind: "pending"
});

constructor(options: ServiceOptions<References>) {
super();
Expand All @@ -53,6 +56,14 @@ export class KeycloakAuthPlugin
this.#logoutOptions = { redirectUri: undefined };
this.#loginOptions = { redirectUri: undefined };

// Backwards compatibility: emit "changed" event when the state changes
this.#watcher = watch(
() => [this.#state.value],
() => {
this.emit("changed");
}
);

try {
this.#keycloakOptions = getKeycloakConfig(options.properties);
} catch (e) {
Expand All @@ -65,14 +76,15 @@ export class KeycloakAuthPlugin
throw new Error("Failed to construct keycloak instance", { cause: e });
}
this.#init().catch((e) => {
// TODO: Handle error
//
// Stay in pending state when an error happens.
// There is currently no useful way to signal an error using the plugin API,
// going into 'not-authenticated' could lead to unexpected behavior (e.g. redirect loops).
// See https://github.com/open-pioneer/trails-core-packages/issues/47
this.#state = {
this.#updateState({
kind: "pending"
};
this.emit("changed");
});
this.#notifier.notify({
level: "error",
title: this.#intl.formatMessage({
Expand All @@ -89,11 +101,12 @@ export class KeycloakAuthPlugin

destroy() {
clearInterval(this.#timerId);
this.#watcher = destroyResource(this.#watcher);
this.#timerId = undefined;
}

getAuthState(): AuthState {
return this.#state;
return this.#state.value;
}

getLoginBehavior(): LoginBehavior {
Expand Down Expand Up @@ -128,7 +141,7 @@ export class KeycloakAuthPlugin
}

if (isAuthenticated) {
this.#state = {
this.#updateState({
kind: "authenticated",
sessionInfo: {
userId: this.#keycloak.subject ? this.#keycloak.subject : "undefined",
Expand All @@ -140,8 +153,7 @@ export class KeycloakAuthPlugin
userName: this.#keycloak.idTokenParsed?.preferred_username
}
}
};
this.emit("changed");
});

LOG.debug(`User ${this.#keycloak.subject} is authenticated`);

Expand All @@ -150,10 +162,9 @@ export class KeycloakAuthPlugin
this.__refresh(refreshOptions.interval, refreshOptions.timeLeft);
}
} else {
this.#state = {
this.#updateState({
kind: "not-authenticated"
};
this.emit("changed");
});
LOG.debug("User is not authenticated");
}
}
Expand All @@ -164,14 +175,18 @@ export class KeycloakAuthPlugin
this.#timerId = setInterval(() => {
this.#keycloak.updateToken(timeLeft).catch((e) => {
LOG.error("Failed to refresh token", e);
this.#state = {
this.#updateState({
kind: "not-authenticated"
};
});
this.emit("changed");
this.destroy();
});
}, interval);
}

#updateState(newState: AuthState) {
this.#state.value = newState;
}
}

const DEFAULT_AUTO_REFRESH_OPT = {
Expand Down
3 changes: 2 additions & 1 deletion src/packages/authentication-keycloak/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
"build": "build-pioneer-package"
},
"peerDependencies": {
"@conterra/reactivity-core": "^0.4.0",
"@open-pioneer/authentication": "workspace:^",
"@open-pioneer/runtime": "workspace:^",
"@open-pioneer/core": "workspace:^",
"@open-pioneer/notifier": "workspace:^",
"keycloak-js": "^23.0.7"
"keycloak-js": "^25.0.2"
},
"devDependencies": {
"core-packages": "workspace:^"
Expand Down
14 changes: 14 additions & 0 deletions src/packages/reactivity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ export class Model {
}
```

#### Rendering a single reactive value

If you have a single signal at hand, there is no need to use the more complex `useReactiveSnapshot` hook.
You can use the more primitive `useReactiveValue` hook instead:

```jsx
import { useReactiveValue } from "@open-pioneer/reactivity";

function YourComponent({ signal }) {
const value = useReactiveValue(signal); // Subscribes to `signal.value` and re-renders on changes.
return <div>{value}</div>;
}
```

More details are available in this package's API documentation and the [README of @conterra/reactivity-core](https://www.npmjs.com/package/@conterra/reactivity-core).

### ESLint configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export class ApplicationContextImpl implements ApplicationContext {
}

setLocale(locale: string): void {
// This restarts the application at the moment, so this.locale will _not_ be updated.
// Instead, we get a new application with a new application context.
this.#changeLocale(locale);
}

Expand Down
2 changes: 1 addition & 1 deletion src/samples/keycloak-sample/AppUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function AppUI() {
</Text>
<Text>
The following environment properties should be set via vite (e.g. in{" "}
<Code>env.local</Code>):
<Code>.env.local</Code>):
</Text>
<UnorderedList>
<ListItem>
Expand Down
50 changes: 50 additions & 0 deletions src/samples/keycloak-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# keycloak-sample

Testing keycloak locally with docker.

## Docker container

Create a local docker container, for example by using the following command:

```bash
# Note: --rm will automatically delete your container when it exits
$ docker run --rm -it -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev
```

## Creating a client application

Open <http://localhost:8080> and log in with `admin` / `admin`.

Visit the "Clients" section of the admin console and click "Create client":

![New Client](./testing/New_Client_1.png)

Authentication can be left to default settings:

![Client Settings](./testing/New_Client_2.png)

Configure the URLs of your local application.
We're using the path to the keycloak sample app in the following example.

Note that the Vite server must run on the exact port used here (you can change the vite config to force a specific port, if necessary).

![Client URLs](./testing/New_Client_3.png)

Finally, save the new client configuration.

## Configuring the sample app

Create a file called `.env.local` in the root of this repository with the following content:

```
VITE_KEYCLOAK_CONFIG_URL=http://localhost:8080
VITE_KEYCLOAK_CONFIG_REALM=master
VITE_KEYCLOAK_CONFIG_CLIENT_ID=trails
```

Afterwards, restart vite and open the keycloak sample app in your browser.
It should now redirect you to your local keycloak and back to the sample app once you are authenticated.

![Authenticated](./testing/Authenticated.png)

Clicking the "Logout" button should end the session.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f48bb02

Please sign in to comment.