Skip to content

Commit

Permalink
feat: implement timeout warning for SSO token timeout
Browse files Browse the repository at this point in the history
feat: implement timeout warning for SSO token timeout
  • Loading branch information
timisenco2015 authored Oct 10, 2023
2 parents e52f955 + 2f93d00 commit 3b3cd06
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 10 deletions.
89 changes: 89 additions & 0 deletions app/frontend/package-lock.json

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

1 change: 1 addition & 0 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^2.0.10",
"@mdi/font": "^6.9.96",
"@vueuse/core": "^10.5.0",
"axios": "^0.27.2",
"bootstrap-scss": "^4.6.2",
"core-js": "^3.30.2",
Expand Down
3 changes: 3 additions & 0 deletions app/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<transition name="component-fade" mode="out-in">
<router-view />
</transition>
<BaseWarningDialog />
</v-main>

<BCGovFooter />
Expand All @@ -18,13 +19,15 @@
import BCGovHeader from '@/components/bcgov/BCGovHeader.vue';
import BCGovFooter from '@/components/bcgov/BCGovFooter.vue';
import BCGovNavBar from '@/components/bcgov/BCGovNavBar.vue';
import BaseWarningDialog from '@/components/base/BaseWarningDialog.vue';
export default {
name: 'App',
components: {
BCGovHeader,
BCGovFooter,
BCGovNavBar,
BaseWarningDialog,
},
};
</script>
48 changes: 48 additions & 0 deletions app/frontend/src/components/base/BaseWarningDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<BaseDialog
type="CONTINUE"
:showCloseButton="true"
:width="'50%'"
:value="showTokenExpiredWarningMSg"
@close-dialog="
() => {
setTokenExpirationWarningDialog({
showTokenExpiredWarningMSg: false,
resetToken: false,
});
}
"
@continue-dialog="
() => {
setTokenExpirationWarningDialog({
showTokenExpiredWarningMSg: false,
resetToken: true,
});
}
"
>
<template #title><span>Session expiring</span></template>
<template #text>
<div class="text-display-4">
Your session will expire soon and you will be signed out automatically.
</div>
<div class="text-display-3 mt-3">Do you want to stay signed in?</div>
</template>
<template #button-text-continue>
<span>Confirm</span>
</template>
</BaseDialog>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'BaseWarningDialog',
computed: {
...mapGetters('auth', ['showTokenExpiredWarningMSg']),
},
methods: {
...mapActions('auth', ['setTokenExpirationWarningDialog']),
},
};
</script>
14 changes: 4 additions & 10 deletions app/frontend/src/plugins/keycloak.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export default {
loginFn: null,
login: null,
createLoginUrl: null,
updateToken: null,
clearToken: null,
createLogoutUrl: null,
createRegisterUrl: null,
register: null,
Expand Down Expand Up @@ -81,17 +83,7 @@ function init(config, watch, options) {
watch.$emit('ready', options.onReady.bind(this, keycloak));
};
keycloak.onAuthSuccess = function () {
// Check token validity every 10 seconds (10 000 ms) and, if necessary, update the token.
// Refresh token if it's valid for less then 60 seconds
const updateTokenInterval = setInterval(
() =>
keycloak.updateToken(60).catch(() => {
keycloak.clearToken();
}),
10000
);
watch.logoutFn = () => {
clearInterval(updateTokenInterval);
keycloak.logout(
options.logout || { redirectUri: config['logoutRedirectUri'] }
);
Expand Down Expand Up @@ -129,6 +121,8 @@ function init(config, watch, options) {
watch.realmAccess = keycloak.realmAccess;
watch.resourceAccess = keycloak.resourceAccess;
watch.refreshToken = keycloak.refreshToken;
watch.updateToken = keycloak.updateToken;
watch.clearToken = keycloak.clearToken;
watch.refreshTokenParsed = keycloak.refreshTokenParsed;
watch.timeSkew = keycloak.timeSkew;
watch.responseMode = keycloak.responseMode;
Expand Down
1 change: 1 addition & 0 deletions app/frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ export default function getRouter(basePath = '/') {
router.app.$keycloak.authenticated
) {
store.dispatch('form/getFormsForCurrentUser');
store.dispatch('auth/checkTokenExpiration');
}

// Handle proper redirections on first page load
Expand Down
70 changes: 70 additions & 0 deletions app/frontend/src/store/modules/auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Vue from 'vue';
import getRouter from '@/router';
import { useIdle, useTimestamp, watchPausable } from '@vueuse/core';
import { ref } from 'vue';
import moment from 'moment';

/**
* @function hasRoles
Expand All @@ -20,13 +23,20 @@ export default {
// In most cases, when this becomes populated, we end up doing a redirect flow,
// so when we return to the app, it is fresh again and undefined
redirectUri: undefined,
showTokenExpiredWarningMSg: false,
inActiveCheckInterval: null,
updateTokenInterval: null,
watchPausable: null,
},
getters: {
authenticated: () => Vue.prototype.$keycloak.authenticated,
createLoginUrl: () => (options) =>
Vue.prototype.$keycloak.createLoginUrl(options),
createLogoutUrl: () => (options) =>
Vue.prototype.$keycloak.createLogoutUrl(options),
updateToken: () => (minValidity) =>
Vue.prototype.$keycloak.updateToken(minValidity),
clearToken: () => () => Vue.prototype.$keycloak.clearToken(),
email: () =>
Vue.prototype.$keycloak.tokenParsed
? Vue.prototype.$keycloak.tokenParsed.email
Expand Down Expand Up @@ -83,11 +93,17 @@ export default {

return user;
},
showTokenExpiredWarningMSg: (state) => state.showTokenExpiredWarningMSg,
inActiveCheckInterval: (state) => state.inActiveCheckInterval,
updateTokenInterval: (state) => state.updateTokenInterval,
},
mutations: {
SET_REDIRECTURI(state, redirectUri) {
state.redirectUri = redirectUri;
},
SET_SHOW_TOKEN_EXPIRED_WARNING_MSG(state, showTokenExpiredWarningMSg) {
state.showTokenExpiredWarningMSg = showTokenExpiredWarningMSg;
},
},
actions: {
// TODO: Ideally move this to notifications module, but some strange interactions with lazy loading in unit tests
Expand Down Expand Up @@ -141,5 +157,59 @@ export default {
);
}
},
async setTokenExpirationWarningDialog(
{ getters, commit, dispatch, state },
{ showTokenExpiredWarningMSg, resetToken }
) {
if (!showTokenExpiredWarningMSg && resetToken) {
state.watchPausable.resume();
getters.updateToken(-1).catch(() => {
getters.clearToken();
dispatch('logout');
});
} else if (!resetToken) {
clearInterval(getters.updateTokenInterval);
clearInterval(getters.inActiveCheckInterval);
dispatch('logout');
}
commit('SET_SHOW_TOKEN_EXPIRED_WARNING_MSG', showTokenExpiredWarningMSg);
if (showTokenExpiredWarningMSg) {
setTimeout(() => {
dispatch('logout');
}, 180000);
}
},
async checkTokenExpiration({ getters, dispatch, state }) {
if (getters.authenticated) {
const { idle, lastActive } = useIdle(1000, { initialState: true });
const source = ref(idle);
const now = useTimestamp({ interval: 1000 });
state.watchPausable = watchPausable(source, (value) => {
if (value) {
clearInterval(getters.updateTokenInterval);
state.inActiveCheckInterval = setInterval(() => {
let end = moment(now.value);
let active = moment(lastActive.value);
let duration = moment.duration(end.diff(active)).as('minutes');
if (duration > 1) {
state.watchPausable.pause();
dispatch('setTokenExpirationWarningDialog', {
showTokenExpiredWarningMSg: true,
resetToken: true,
});
}
}, 60000);
} else {
clearInterval(getters.inActiveCheckInterval);
state.updateTokenInterval = setInterval(() => {
getters.updateToken(-1).catch(() => {
getters.clearToken();
});
}, 120000);
}
});
state.watchPausable.resume();
}
},
},
};

0 comments on commit 3b3cd06

Please sign in to comment.