Skip to content

Commit

Permalink
update keycloak configuration to use another name: oidc.
Browse files Browse the repository at this point in the history
allow null logoutUrl.
add parameters to logoutUrl at runtime

Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed Feb 28, 2024
1 parent 879f6b5 commit d746ebc
Show file tree
Hide file tree
Showing 14 changed files with 93 additions and 64 deletions.
21 changes: 12 additions & 9 deletions .devcontainer/chefs_local/local.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@
"frontend": {
"apiPath": "api/v1",
"basePath" : "/app",
"keycloak": {
"clientId": "chefs-frontend-local",
"realm": "chefs",
"serverUrl": "http://localhost:8082"
"oidc": {
"clientId": "chefs-frontend-localhost-5300",
"realm": "standard",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth",
"logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout"
}
},
"server": {
"apiPath": "/api/v1",
"basePath" : "/app",
"bodyLimit": "30mb",
"keycloak": {
"clientId": "chefs",
"realm": "chefs",
"serverUrl": "http://localhost:8082",
"clientSecret": "XXXXXXXXXXXX"
"oidc": {
"realm": "standard",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth",
"jwksUri": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs",
"issuer": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard",
"audience": "chefs-frontend-localhost-5300",
"maxTokenAge": "300"
},
"logLevel": "http",
"port": "8080",
Expand Down
23 changes: 12 additions & 11 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,24 @@
"adminDashboardUrl": "VITE_ADMIN_DASHBOARD_URL",
"apiPath": "FRONTEND_APIPATH",
"basePath": "VITE_FRONTEND_BASEPATH",
"keycloak": {
"clientId": "FRONTEND_KC_CLIENTID",
"realm": "FRONTEND_KC_REALM",
"serverUrl": "FRONTEND_KC_SERVERURL",
"logoutUrl": "FRONTEND_KC_LOGOUTURL"
"oidc": {
"clientId": "OIDC_CLIENTID",
"realm": "OIDC_REALM",
"serverUrl": "OIDC_SERVERURL",
"logoutUrl": "OIDC_LOGOUTURL"
}
},
"server": {
"apiPath": "SERVER_APIPATH",
"basePath": "SERVER_BASEPATH",
"bodyLimit": "SERVER_BODYLIMIT",
"keycloak": {
"serverUrl": "SERVER_KC_SERVERURL",
"jwksUri": "SERVER_KC_JWKSURI",
"issuer": "SERVER_KC_ISSUER",
"audience": "SERVER_KC_AUDIENCE",
"maxTokenAge": "SERVER_KC_MAXTOKENAGE"
"oidc": {
"realm": "OIDC_REALM",
"serverUrl": "OIDC_SERVERURL",
"jwksUri": "OIDC_JWKSURI",
"issuer": "OIDC_ISSUER",
"audience": "OIDC_CLIENTID",
"maxTokenAge": "OIDC_MAXTOKENAGE"
},
"logFile": "SERVER_LOGFILE",
"logLevel": "SERVER_LOGLEVEL",
Expand Down
12 changes: 6 additions & 6 deletions app/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,25 @@
"adminDashboardUrl": "",
"apiPath": "api/v1",
"basePath": "/app",
"keycloak": {
"clientId": "chefs-frontend",
"oidc": {
"clientId": "chefs-frontend-localhost-5300",
"realm": "standard",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth",
"logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout%3Fpost_logout_redirect_uri%3Dhttp%3A%2F%2Flocalhost%3A5173%2Fapp%26client_id%3Dchefs-frontend"
"logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout"
}
},
"server": {
"apiPath": "/api/v1",
"basePath": "/app",
"bodyLimit": "30mb",
"keycloak": {
"oidc": {
"realm": "standard",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth",
"jwksUri": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs",
"issuer": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard",
"audience": "chefs-frontend",
"audience": "chefs-frontend-localhost-5300",
"maxTokenAge": "300"
},
},
"logLevel": "http",
"port": "8080",
"rateLimit": {
Expand Down
2 changes: 1 addition & 1 deletion app/config/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"server": {
"emailRecipients": "[email protected],[email protected]",
"keycloak": {
"oidc": {
"clientSecret": "password"
},
"logLevel": "silent"
Expand Down
13 changes: 0 additions & 13 deletions app/frontend/src/components/admin/AdministerUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ export default {
...mapState(useAppStore, ['config']),
...mapState(useAdminStore, ['user']),
...mapState(useFormStore, ['lang']),
userUrl() {
return `${this.config.keycloak.serverUrl}/admin/${this.config.keycloak.realm}/console/#/realms/${this.config.keycloak.realm}/users/${this.user.keycloakId}`;
},
},
async mounted() {
await this.readUser(this.userId);
Expand All @@ -34,15 +31,5 @@ export default {
<h3>{{ user.fullName }}</h3>
<h4 :lang="lang">{{ $t('trans.administerUser.userDetails') }}</h4>
<pre>{{ user }}</pre>

<v-btn
color="primary"
variant="text"
size="small"
:href="userUrl"
target="_blank"
>
<span :lang="lang">{{ $t('trans.administerUser.openSSOConsole') }}</span>
</v-btn>
</div>
</template>
17 changes: 8 additions & 9 deletions app/frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,10 @@ async function loadConfig() {

if (
!config ||
!config.keycloak ||
!config.keycloak.clientId ||
!config.keycloak.realm ||
!config.keycloak.serverUrl ||
!config.keycloak.logoutUrl
!config.oidc ||
!config.oidc.clientId ||
!config.oidc.realm ||
!config.oidc.serverUrl
) {
throw new Error('Keycloak is misconfigured');
}
Expand All @@ -168,9 +167,9 @@ function loadKeycloak(config) {
const options = Object.assign({}, defaultParams, {
init: { pkceMethod: 'S256', checkLoginIframe: false, onLoad: 'check-sso' },
config: {
clientId: config.keycloak.clientId,
realm: config.keycloak.realm,
url: config.keycloak.serverUrl,
clientId: config.oidc.clientId,
realm: config.oidc.realm,
url: config.oidc.serverUrl,
},
onReady: () => {
initializeApp(true, config.basePath);
Expand All @@ -189,7 +188,7 @@ function loadKeycloak(config) {
const ctor = sanitizeConfig(cfg);

const authStore = useAuthStore();
authStore.logoutUrl = config.keycloak.logoutUrl;
authStore.logoutUrl = config.oidc.logoutUrl;

keycloak = new Keycloak(ctor);
keycloak.onReady = (authenticated) => {
Expand Down
19 changes: 18 additions & 1 deletion app/frontend/src/store/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia';
import getRouter from '~/router';
import { useIdpStore } from '~/store/identityProviders';
import { useAppStore } from '~/store/app';

/**
* @function hasRoles
Expand All @@ -26,6 +27,8 @@ export const useAuthStore = defineStore('auth', {
getters: {
createLoginUrl: (state) => (options) =>
state.keycloak.createLoginUrl(options),
createLogoutUrl: (state) => (options) =>
state.keycloak.createLogoutUrl(options),
email: (state) =>
state.keycloak.tokenParsed ? state.keycloak.tokenParsed.email : '',
fullName: (state) => state.keycloak.tokenParsed.name,
Expand Down Expand Up @@ -123,7 +126,21 @@ export const useAuthStore = defineStore('auth', {
},
logout() {
if (this.ready) {
window.location.assign(this.logoutUrl);
// if we have not specified a logoutUrl, then use default
if (!this.logoutUrl) {
window.location.replace(
this.createLogoutUrl({
redirectUri: location.origin,
})
);
} else {
const appStore = useAppStore();
const cli_param = `client_id=${this.keycloak.clientId}`;
const redirect_param = `post_logout_redirect_uri=${location.origin}${appStore.config.basePath}`;
const logout_param = `${redirect_param}&${cli_param}`;
let logout = `${this.logoutUrl}?${encodeURIComponent(logout_param)}`;
window.location.assign(logout);
}
}
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('AdministerUser.vue', () => {

it('renders', async () => {
appStore.config = {
keycloak: {
oidc: {
serverUrl: 'servU',
realm: 'theRealm',
},
Expand All @@ -43,8 +43,5 @@ describe('AdministerUser.vue', () => {

await flushPromises();
expect(wrapper.text()).toContain('alice');
expect(wrapper.html()).toContain(
'servU/admin/theRealm/console/#/realms/theRealm/users/1'
);
});
});
15 changes: 12 additions & 3 deletions app/frontend/tests/unit/components/base/BaseAuthButton.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@

import { mount } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import { vi } from 'vitest';
import { expect, vi } from 'vitest';

import getRouter from '~/router';
import BaseAuthButton from '~/components/base/BaseAuthButton.vue';
import { useAuthStore } from '~/store/auth';
import { useIdpStore } from '~/store/identityProviders';
import { useAppStore } from '~/store/app';

describe('BaseAuthButton.vue', () => {
const pinia = createPinia();
setActivePinia(pinia);
const authStore = useAuthStore();
const idpStore = useIdpStore();
const appStore = useAppStore();
const router = getRouter();
const windowReplaceSpy = vi.spyOn(window.location, 'assign');
idpStore.providers = require('../../fixtures/identityProviders.json');

beforeEach(async () => {
windowReplaceSpy.mockReset();
appStore.$reset();
appStore.config = {
basePath: '/app'
};
authStore.$reset();
authStore.keycloak = {
createLoginUrl: vi.fn((opts) => opts),
clientId: 'clientid'
};
router.currentRoute.value.meta.hasLogin = true;
router.push('/');
Expand Down Expand Up @@ -103,7 +110,8 @@ describe('BaseAuthButton.vue', () => {

it('logout button redirects to logout url', async () => {
authStore.authenticated = true;
authStore.logoutUrl = location.origin;
authStore.logoutUrl = 'http://redirect.com/logout';
authStore.keycloak
authStore.ready = true;
const wrapper = mount(BaseAuthButton, {
global: {
Expand All @@ -114,6 +122,7 @@ describe('BaseAuthButton.vue', () => {
wrapper.vm.logout();
expect(wrapper.text()).toMatch('trans.baseAuthButton.logout');
expect(windowReplaceSpy).toHaveBeenCalledTimes(1);
expect(windowReplaceSpy).toHaveBeenCalledWith(location.origin);
const params = encodeURIComponent(`post_logout_redirect_uri=null/app&client_id=clientid`)
expect(windowReplaceSpy).toHaveBeenCalledWith(`http://redirect.com/logout?${params}`);
});
});
3 changes: 3 additions & 0 deletions app/frontend/tests/unit/store/modules/auth.actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import getRouter from '~/router';
import { useAuthStore } from '~/store/auth';
import { useFormStore } from '~/store/form';
import { useIdpStore } from '~/store/identityProviders';
import { useAppStore } from '~/store/app';

describe('auth actions', () => {
let router = getRouter();
Expand All @@ -17,6 +18,7 @@ describe('auth actions', () => {
const mockStore = useAuthStore();
const formStore = useFormStore();
const idpStore = useIdpStore();
const appStore = useAppStore();

idpStore.providers = require('../../fixtures/identityProviders.json');

Expand All @@ -31,6 +33,7 @@ describe('auth actions', () => {
replaceSpy.mockReset();
windowReplaceSpy.mockReset();
router.replace.mockReset();
appStore.config = { basePath: '/app' };
});

it('should do nothing if keycloak is not ready', () => {
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/jwtService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const errorToProblem = require('./errorToProblem');

const SERVICE = 'JwtService';

const jwksUri = config.get('server.keycloak.jwksUri');
const jwksUri = config.get('server.oidc.jwksUri');

// Create a remote JWK set that fetches the JWK set from server with caching
const JWKS = jose.createRemoteJWKSet(new URL(jwksUri));
Expand Down Expand Up @@ -88,9 +88,9 @@ class JwtService {
}
}

const audience = config.get('server.keycloak.audience');
const issuer = config.get('server.keycloak.issuer');
const maxTokenAge = config.get('server.keycloak.maxTokenAge');
const audience = config.get('server.oidc.audience');
const issuer = config.get('server.oidc.issuer');
const maxTokenAge = config.get('server.oidc.maxTokenAge');

let jwtService = new JwtService({
issuer: issuer,
Expand Down
4 changes: 1 addition & 3 deletions app/src/routes/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ const getSpec = () => {
const rawSpec = fs.readFileSync(path.join(__dirname, '../docs/v1.api-spec.yaml'), 'utf8');
const spec = yaml.load(rawSpec);
spec.servers[0].url = `${config.get('server.basePath')}/api/v1`;
spec.components.securitySchemes.OpenID.openIdConnectUrl = `${config.get('server.keycloak.serverUrl')}/realms/${config.get(
'server.keycloak.realm'
)}/.well-known/openid-configuration`;
spec.components.securitySchemes.OpenID.openIdConnectUrl = `${config.get('server.oidc.serverUrl')}/realms/${config.get('server.oidc.realm')}/.well-known/openid-configuration`;
return spec;
};

Expand Down
13 changes: 13 additions & 0 deletions openshift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ oc create -n $NAMESPACE configmap $APP_NAME-server-config \
--from-literal=SERVER_PORT=8080
```

_Note:_ OIDC config is for moving from a custom Keycloak realm into the BC Gov standard realm a managed SSO platform. Other KC configuration will be deprecated. Urls and Client IDs will change from environment to environment.

```sh
oc create -n $NAMESPACE configmap $APP_NAME-oidc-config \
--from-literal=OIDC_REALM=standard \
--from-literal=OIDC_SERVERURL=https://dev.loginproxy.gov.bc.ca/auth \
--from-literal=OIDC_JWKSURI=https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs \
--from-literal=OIDC_ISSUER=https://dev.loginproxy.gov.bc.ca/auth/realms/standard \
--from-literal=OIDC_CLIENTID=chefs-frontend-5299 \
--from-literal=OIDC_MAXTOKENAGE=300 \
--from-literal=OIDC_LOGOUTURL='https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout'
```

_Note:_ We use the Common Services Object Storage for CHEFS. You will need to contact them to have your storage bucket created.

```sh
Expand Down
2 changes: 2 additions & 0 deletions openshift/app.dc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ objects:
name: "${APP_NAME}-service-config"
- configMapRef:
name: "${APP_NAME}-files-config"
- configMapRef:
name: "${APP_NAME}-oidc-config"
- configMapRef:
name: "${APP_NAME}-custombcaddressformiocomponent-config"
restartPolicy: Always
Expand Down

0 comments on commit d746ebc

Please sign in to comment.