From 3a5fe1e027170ab6e6c4e92563a6408f63cf32f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Raddaoui=20Mar=C3=ADn?= Date: Fri, 12 Jul 2024 16:24:48 +0200 Subject: [PATCH] End OIDC sessions upstream on signout - Follow signout redirect/callback flow to end sessions upstream. - Change environment configuration to set a base URL and generate the signin and signout redirect URLs from it. --- dashboard/.env | 2 +- dashboard/.env.development | 2 +- dashboard/.env.test | 2 +- dashboard/src/components/Sidebar.vue | 2 +- dashboard/src/env.d.ts | 2 +- dashboard/src/pages/user/signout-callback.vue | 10 ++++ dashboard/src/stores/__tests__/auth.test.ts | 60 ++++++++++++++++--- dashboard/src/stores/auth.ts | 27 ++++++--- dashboard/typed-router.d.ts | 1 + hack/kube/base/enduro-dashboard.yaml | 10 ++-- .../dev/enduro-dashboard-secret.yaml | 2 +- hack/kube/components/dev/keycloak.yaml | 4 ++ 12 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 dashboard/src/pages/user/signout-callback.vue diff --git a/dashboard/.env b/dashboard/.env index 9767194c7..216c1b2ae 100644 --- a/dashboard/.env +++ b/dashboard/.env @@ -1,7 +1,7 @@ VITE_OIDC_ENABLED=\$VITE_OIDC_ENABLED +VITE_OIDC_BASE_URL=\$VITE_OIDC_BASE_URL VITE_OIDC_AUTHORITY=\$VITE_OIDC_AUTHORITY VITE_OIDC_CLIENT_ID=\$VITE_OIDC_CLIENT_ID -VITE_OIDC_REDIRECT_URI=\$VITE_OIDC_REDIRECT_URI VITE_OIDC_EXTRA_SCOPES=\$VITE_OIDC_EXTRA_SCOPES VITE_OIDC_EXTRA_QUERY_PARAMS=\$VITE_OIDC_EXTRA_QUERY_PARAMS VITE_OIDC_ABAC_ENABLED=\$VITE_OIDC_ABAC_ENABLED diff --git a/dashboard/.env.development b/dashboard/.env.development index 93a9a4709..99c78e726 100644 --- a/dashboard/.env.development +++ b/dashboard/.env.development @@ -1,7 +1,7 @@ VITE_OIDC_ENABLED=true +VITE_OIDC_BASE_URL=http://localhost:8080 VITE_OIDC_AUTHORITY=http://keycloak:7470/realms/artefactual VITE_OIDC_CLIENT_ID=enduro -VITE_OIDC_REDIRECT_URI=http://localhost:8080/user/signin-callback VITE_OIDC_EXTRA_SCOPES=enduro VITE_OIDC_EXTRA_QUERY_PARAMS= VITE_OIDC_ABAC_ENABLED=true diff --git a/dashboard/.env.test b/dashboard/.env.test index b88aeca35..34bb6d5ca 100644 --- a/dashboard/.env.test +++ b/dashboard/.env.test @@ -1,7 +1,7 @@ VITE_OIDC_ENABLED=true +VITE_OIDC_BASE_URL=http://localhost:8080 VITE_OIDC_AUTHORITY=http://keycloak:7470/realms/artefactual VITE_OIDC_CLIENT_ID=enduro -VITE_OIDC_REDIRECT_URI=http://localhost:8080/user/signin-callback VITE_OIDC_EXTRA_SCOPES=enduro VITE_OIDC_EXTRA_QUERY_PARAMS="audience=enduro-api, key = value" VITE_OIDC_ABAC_ENABLED=true diff --git a/dashboard/src/components/Sidebar.vue b/dashboard/src/components/Sidebar.vue index a4061a52d..52971e45c 100644 --- a/dashboard/src/components/Sidebar.vue +++ b/dashboard/src/components/Sidebar.vue @@ -148,7 +148,7 @@ onMounted(() => {
diff --git a/dashboard/src/env.d.ts b/dashboard/src/env.d.ts index 69abc3db9..364f5d9c2 100644 --- a/dashboard/src/env.d.ts +++ b/dashboard/src/env.d.ts @@ -12,9 +12,9 @@ declare module "*.vue" { interface ImportMetaEnv { readonly VITE_OIDC_ENABLED: string; + readonly VITE_OIDC_BASE_URL: string; readonly VITE_OIDC_AUTHORITY: string; readonly VITE_OIDC_CLIENT_ID: string; - readonly VITE_OIDC_REDIRECT_URI: string; readonly VITE_OIDC_EXTRA_SCOPES: string; readonly VITE_OIDC_EXTRA_QUERY_PARAMS: string; readonly VITE_OIDC_ABAC_ENABLED: string; diff --git a/dashboard/src/pages/user/signout-callback.vue b/dashboard/src/pages/user/signout-callback.vue new file mode 100644 index 000000000..1c40fd722 --- /dev/null +++ b/dashboard/src/pages/user/signout-callback.vue @@ -0,0 +1,10 @@ + + + diff --git a/dashboard/src/stores/__tests__/auth.test.ts b/dashboard/src/stores/__tests__/auth.test.ts index ad3b6b652..bba32923c 100644 --- a/dashboard/src/stores/__tests__/auth.test.ts +++ b/dashboard/src/stores/__tests__/auth.test.ts @@ -22,9 +22,9 @@ describe("useAuthStore", () => { authStore.$patch((state) => { state.config = { enabled: true, + baseUrl: "", provider: "", clientId: "", - redirectUrl: "", extraScopes: "", extraQueryParams: "", abac: { @@ -187,9 +187,9 @@ describe("useAuthStore", () => { authStore.$patch((state) => { state.config = { enabled: true, + baseUrl: "", provider: "", clientId: "", - redirectUrl: "", extraScopes: "", extraQueryParams: "", abac: { @@ -210,9 +210,9 @@ describe("useAuthStore", () => { authStore.loadConfig(); expect(authStore.config).toEqual({ enabled: true, + baseUrl: "http://localhost:8080", provider: "http://keycloak:7470/realms/artefactual", clientId: "enduro", - redirectUrl: "http://localhost:8080/user/signin-callback", extraScopes: "enduro", extraQueryParams: "audience=enduro-api, key = value", abac: { @@ -235,6 +235,9 @@ describe("useAuthStore", () => { expect(authStore.manager?.settings.redirect_uri).toEqual( "http://localhost:8080/user/signin-callback", ); + expect(authStore.manager?.settings.post_logout_redirect_uri).toEqual( + "http://localhost:8080/user/signout-callback", + ); expect(authStore.manager?.settings.scope).toEqual( "openid email profile enduro", ); @@ -254,6 +257,23 @@ describe("useAuthStore", () => { expect(authStore.manager).toEqual(null); }); + it("redirects for signin", () => { + const manager = new UserManager({ + authority: "", + client_id: "", + redirect_uri: "", + }); + + const redirectMock = vi.fn().mockImplementation(manager.signinRedirect); + redirectMock.mockImplementation(async () => null); + manager.signinRedirect = redirectMock; + + const authStore = useAuthStore(); + authStore.$patch((state) => (state.manager = manager)); + authStore.signinRedirect(); + expect(redirectMock).toHaveBeenCalledOnce(); + }); + it("receives a signin callback", async () => { const manager = new UserManager({ authority: "", @@ -276,23 +296,47 @@ describe("useAuthStore", () => { expect(authStore.user).toEqual(null); }); - it("redirects for signin", () => { + it("redirects for signout", () => { const manager = new UserManager({ authority: "", client_id: "", redirect_uri: "", }); - const redirectMock = vi.fn().mockImplementation(manager.signinRedirect); + const redirectMock = vi.fn().mockImplementation(manager.signoutRedirect); redirectMock.mockImplementation(async () => null); - manager.signinRedirect = redirectMock; + manager.signoutRedirect = redirectMock; const authStore = useAuthStore(); authStore.$patch((state) => (state.manager = manager)); - authStore.signinRedirect(); + authStore.signoutRedirect(); expect(redirectMock).toHaveBeenCalledOnce(); }); + it("receives a signout callback", async () => { + const manager = new UserManager({ + authority: "", + client_id: "", + redirect_uri: "", + }); + + const callbackMock = vi.fn().mockImplementation(manager.signoutCallback); + callbackMock.mockImplementation(async () => null); + manager.signoutCallback = callbackMock; + + const authStore = useAuthStore(); + authStore.$patch((state) => (state.manager = manager)); + + const removeUserMock = vi.fn().mockImplementation(authStore.removeUser); + removeUserMock.mockImplementation(async () => null); + authStore.removeUser = removeUserMock; + + authStore.signoutCallback(); + await flushPromises(); + expect(callbackMock).toHaveBeenCalledOnce(); + expect(removeUserMock).toHaveBeenCalledOnce(); + }); + it("loads and removes the user and attributes", async () => { var user = new User({ access_token: "", @@ -518,9 +562,9 @@ describe("useAuthStore", () => { authStore.$patch((state) => { state.config = { enabled: true, + baseUrl: "", provider: "", clientId: "", - redirectUrl: "", extraScopes: "", extraQueryParams: "", abac: { diff --git a/dashboard/src/stores/auth.ts b/dashboard/src/stores/auth.ts index 1aa9cb202..6a3f4f649 100644 --- a/dashboard/src/stores/auth.ts +++ b/dashboard/src/stores/auth.ts @@ -5,9 +5,9 @@ import { Buffer } from "buffer"; type OIDCConfig = { enabled: boolean; + baseUrl: string; provider: string; clientId: string; - redirectUrl: string; extraScopes: string; extraQueryParams: string; abac: ABACConfig; @@ -84,9 +84,9 @@ export const useAuthStore = defineStore("auth", { this.config = { enabled: false, + baseUrl: "", provider: "", clientId: "", - redirectUrl: "", extraScopes: "", extraQueryParams: "", abac: { @@ -102,15 +102,15 @@ export const useAuthStore = defineStore("auth", { this.config.enabled = env.VITE_OIDC_ENABLED.trim().toLowerCase() === "true"; } + if (env.VITE_OIDC_BASE_URL) { + this.config.baseUrl = env.VITE_OIDC_BASE_URL.trim(); + } if (env.VITE_OIDC_AUTHORITY) { this.config.provider = env.VITE_OIDC_AUTHORITY.trim(); } if (env.VITE_OIDC_CLIENT_ID) { this.config.clientId = env.VITE_OIDC_CLIENT_ID.trim(); } - if (env.VITE_OIDC_REDIRECT_URI) { - this.config.redirectUrl = env.VITE_OIDC_REDIRECT_URI.trim(); - } if (env.VITE_OIDC_EXTRA_SCOPES) { this.config.extraScopes = env.VITE_OIDC_EXTRA_SCOPES.trim(); } @@ -158,10 +158,15 @@ export const useAuthStore = defineStore("auth", { }); } + if (!this.config.baseUrl.endsWith("/")) { + this.config.baseUrl += "/"; + } + this.manager = new UserManager({ authority: this.config.provider, client_id: this.config.clientId, - redirect_uri: this.config.redirectUrl, + redirect_uri: this.config.baseUrl + "user/signin-callback", + post_logout_redirect_uri: this.config.baseUrl + "user/signout-callback", extraQueryParams: extraQueryParams, scope: scope, userStore: new WebStorageStateStore({ store: window.localStorage }), @@ -175,13 +180,21 @@ export const useAuthStore = defineStore("auth", { this.loadManager(); this.setUser((await this.manager?.signinCallback()) || null); }, + signoutRedirect() { + this.loadManager(); + this.manager?.signoutRedirect(); + }, + async signoutCallback() { + this.loadManager(); + await this.manager?.signoutCallback(); + await this.removeUser(); + }, // Load the currently authenticated user. async loadUser() { this.loadManager(); this.setUser((await this.manager?.getUser()) || null); }, async removeUser() { - // TODO: end session upstream. this.loadManager(); await this.manager?.removeUser(); this.user = null; diff --git a/dashboard/typed-router.d.ts b/dashboard/typed-router.d.ts index 2e888d6cb..a3998e36b 100644 --- a/dashboard/typed-router.d.ts +++ b/dashboard/typed-router.d.ts @@ -32,5 +32,6 @@ declare module 'vue-router/auto-routes' { '/user': RouteRecordInfo<'/user', '/user', Record, Record>, '/user/signin': RouteRecordInfo<'/user/signin', '/user/signin', Record, Record>, '/user/signin-callback': RouteRecordInfo<'/user/signin-callback', '/user/signin-callback', Record, Record>, + '/user/signout-callback': RouteRecordInfo<'/user/signout-callback', '/user/signout-callback', Record, Record>, } } diff --git a/hack/kube/base/enduro-dashboard.yaml b/hack/kube/base/enduro-dashboard.yaml index 4f6709231..1a8f0c30d 100644 --- a/hack/kube/base/enduro-dashboard.yaml +++ b/hack/kube/base/enduro-dashboard.yaml @@ -25,6 +25,11 @@ spec: secretKeyRef: name: enduro-dashboard-secret key: oidc-enabled + - name: VITE_OIDC_BASE_URL + valueFrom: + secretKeyRef: + name: enduro-dashboard-secret + key: oidc-base-url - name: VITE_OIDC_AUTHORITY valueFrom: secretKeyRef: @@ -35,11 +40,6 @@ spec: secretKeyRef: name: enduro-dashboard-secret key: oidc-client-id - - name: VITE_OIDC_REDIRECT_URI - valueFrom: - secretKeyRef: - name: enduro-dashboard-secret - key: oidc-redirect-url - name: VITE_OIDC_EXTRA_SCOPES valueFrom: secretKeyRef: diff --git a/hack/kube/components/dev/enduro-dashboard-secret.yaml b/hack/kube/components/dev/enduro-dashboard-secret.yaml index e75aba7d4..43cd1f5c4 100644 --- a/hack/kube/components/dev/enduro-dashboard-secret.yaml +++ b/hack/kube/components/dev/enduro-dashboard-secret.yaml @@ -5,8 +5,8 @@ metadata: type: Opaque stringData: oidc-enabled: "true" + oidc-base-url: http://localhost:8080 oidc-provider-url: http://keycloak:7470/realms/artefactual - oidc-redirect-url: http://localhost:8080/user/signin-callback oidc-client-id: enduro oidc-extra-scopes: enduro oidc-extra-query-params: "" diff --git a/hack/kube/components/dev/keycloak.yaml b/hack/kube/components/dev/keycloak.yaml index 0e04c7127..6c0ffd772 100644 --- a/hack/kube/components/dev/keycloak.yaml +++ b/hack/kube/components/dev/keycloak.yaml @@ -167,6 +167,10 @@ data: "enabled": true, "publicClient": true, "redirectUris": ["http://localhost:8080/user/signin-callback"], + "attributes": { + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "http://localhost:8080/user/signout-callback" + }, "protocol": "openid-connect", "protocolMappers": [ {