Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #1179 bceid login #1225

Merged
merged 12 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ TypeScript cannot handle type information for `.vue` imports by default, so we r
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:

1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

## Customize configuration
Expand All @@ -38,3 +38,7 @@ npm run dev
```sh
npm run build
```

### Note

For FAM local and dev environment, we connect with TEST identity provider (TEST-IDIR, TEST-BUSINESSBCEID) for login options. The main reason of that is because we don't have any dev business bceid account can used for testing, so we have to use the TEST-BUSINESSBCEID. And it's better to use the same environment for both IDIR and BUSINESSBCEID, so we can config the same logout chain for both. Use TEST-IDIR in local won't impact any login functionality, work same as DEV-IDIR.
14 changes: 10 additions & 4 deletions frontend/public/env.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{ "fam_admin_management_api_base_url": {
{
"fam_admin_management_api_base_url": {
"sensitive": false,
"type": "string",
"value": "http://localhost:8001"
Expand Down Expand Up @@ -31,7 +32,7 @@
"frontend_logout_chain_url": {
"sensitive": false,
"type": "string",
"value": "https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout?redirect_uri="
"value": "https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https://test.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout?redirect_uri="
},
"front_end_redirect_base_url": {
"sensitive": false,
Expand All @@ -41,11 +42,16 @@
"fam_console_idp_name": {
"sensitive": false,
"type": "string",
"value": "DEV-IDIR"
"value": "TEST-IDIR"
},
"fam_console_idp_name_bceid": {
"sensitive": false,
"type": "string",
"value": "TEST-BCEIDBUSINESS"
},
basilv marked this conversation as resolved.
Show resolved Hide resolved
"target_env": {
"sensitive": false,
"type": "string",
"value": "dev"
}
}
}
40 changes: 0 additions & 40 deletions frontend/readme.md

This file was deleted.

3 changes: 1 addition & 2 deletions frontend/src/components/Landing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import TreeLogs from '@/assets/images/tree-logs.jpg';
outlined
label="Login with BCeID"
id="login-bceid-button"
disabled
@click="AuthService.login()"
@click="AuthService.loginBceid()"
>
<Icon icon="login" :size="IconSize.medium" />
</Button>
Expand Down
40 changes: 28 additions & 12 deletions frontend/src/components/common/ProfileSidebar.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import Avatar from 'primevue/avatar';
import Button from '@/components/common/Button.vue';
import { IconSize } from '@/enum/IconEnum';
import { IdpProvider } from '@/enum/IdpEnum';
import authService from '@/services/AuthService';
import LoginUserState from '@/store/FamLoginUserState';
import { profileSidebarState } from '@/store/ProfileSidebarState';
import Avatar from 'primevue/avatar';
import { computed, ref } from 'vue';

const userName = LoginUserState.state.value.famLoginUser!.username;
const initials = userName ? userName.slice(0, 2) : '';
const displayName = LoginUserState.state.value.famLoginUser!.displayName;
const email = LoginUserState.state.value.famLoginUser!.email;
const organization = LoginUserState.state.value.famLoginUser!.organization;
// the IDP Provider has env in it (like DEV-IDIR, DEV-BCEIDBUSINESS), so we need to split and only grab the IDP part
const idpProvider =
LoginUserState.state.value.famLoginUser!.idpProvider?.split('-')[1];
let userType = IdpProvider.IDIR;
if (idpProvider == 'BCEIDBUSINESS') userType = IdpProvider.BCEIDBUSINESS;

// use local loading state, can't use LoadingState instance
// due to logout() is handled by library.
Expand All @@ -26,13 +33,15 @@ const buttonLabel = computed(() => {
});

const adminRoles = computed(() => {
const userAdminRoles = LoginUserState.getUserAdminRoleGroups()
const userAdminRoles = LoginUserState.getUserAdminRoleGroups();
if (userAdminRoles) {
return userAdminRoles.map((adminRole) => {
return adminRole.replace("_", " ")
}).join(", ")
return userAdminRoles
.map((adminRole) => {
return adminRole.replace('_', ' ');
})
.join(', ');
}
})
});
</script>

<template>
Expand Down Expand Up @@ -61,9 +70,14 @@ const adminRoles = computed(() => {
/>
<div class="profile-info">
<p class="profile-name">{{ displayName }}</p>
<p class="profile-idir">IDIR: {{ userName }}</p>
<p class="profile-email">{{ email }}</p>
<p class="profile-admin-level">Granted: <strong>{{ adminRoles }}</strong></p>
<p class="profile-userid">{{ userType }}: {{ userName }}</p>
<p class="profile-organization" v-if="organization">
Organization: {{ organization }}
</p>
<p class="profile-email">Email: {{ email }}</p>
<p class="profile-admin-level">
Granted: <strong>{{ adminRoles }}</strong>
</p>
</div>
</div>
<hr class="profile-divider" />
Expand Down Expand Up @@ -143,7 +157,8 @@ const adminRoles = computed(() => {
}

.profile-name,
.profile-idir,
.profile-userid,
.profile-organization,
.profile-email {
margin-bottom: 0.375rem;
}
Expand Down Expand Up @@ -177,7 +192,8 @@ const adminRoles = computed(() => {
outline: none !important;
}

.profile-idir,
.profile-userid,
.profile-organization,
.profile-email,
.profile-admin-level,
.options {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/enum/IdpEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum IdpProvider {
IDIR = 'IDIR',
BCEIDBUSINESS = 'Business BCeID',
}
14 changes: 10 additions & 4 deletions frontend/src/services/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ const login = async () => {
*/

const environmentSettings = new EnvironmentSettings();
Auth.federatedSignIn({
customProvider: environmentSettings.getIdentityProviderIdir(),
});
};

const loginBceid = async () => {
const environmentSettings = new EnvironmentSettings();
Auth.federatedSignIn({
customProvider: environmentSettings.getIdentityProvider(),
customProvider: environmentSettings.getIdentityProviderBceid(),
});
};

Expand All @@ -38,7 +44,6 @@ const handlePostLogin = async () => {
// This is to update the FamLoginUser for FamLoginUser.accesses.
// For now team decided to grab user's access only when user login and may change later.
await LoginUserState.cacheUserAccess();

} catch (error) {
console.log('Not signed in');
console.log('Authentication Error:', error);
Expand Down Expand Up @@ -69,7 +74,6 @@ const refreshToken = async (): Promise<FamLoginUser | undefined> => {
if (accesses) famLoginUser.accesses = accesses;
LoginUserState.storeFamUser(famLoginUser);
return famLoginUser;

} catch (error) {
console.error(
'Problem refreshing token or token is invalidated:',
Expand All @@ -92,8 +96,9 @@ const parseToken = (authToken: CognitoUserSession): FamLoginUser => {
username: decodedIdToken['custom:idp_username'],
displayName: decodedIdToken['custom:idp_display_name'],
email: decodedIdToken['email'],
idpProvider: decodedIdToken['identities']['providerName'],
idpProvider: decodedIdToken['identities'][0]['providerName'],
authToken: authToken,
organization: decodedIdToken['custom:idp_business_name'],
};
return famLoginUser;
};
Expand All @@ -102,6 +107,7 @@ const parseToken = (authToken: CognitoUserSession): FamLoginUser => {

export default {
login,
loginBceid,
isLoggedIn,
handlePostLogin,
logout,
Expand Down
22 changes: 13 additions & 9 deletions frontend/src/services/EnvironmentSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export class EnvironmentSettings {
private environmentDisplayNameKey: string = 'fam_environment_display_name';

private readonly API = {
ADMIN_MANAGEMENT_API: "admin_management_api",
APP_ACCESS_CONTROL_API: "app_access_control_api"
ADMIN_MANAGEMENT_API: 'admin_management_api',
APP_ACCESS_CONTROL_API: 'app_access_control_api',
};

constructor() {
Expand All @@ -25,10 +25,14 @@ export class EnvironmentSettings {
}
}

getIdentityProvider(): string {
getIdentityProviderIdir(): string {
return this.env?.fam_console_idp_name.value;
}

getIdentityProviderBceid(): string {
return this.env?.fam_console_idp_name_bceid.value;
}

// Admin Management API
getAdminMgmtApiBaseUrl(): string {
return this.getApiBaseUrl(this.API.ADMIN_MANAGEMENT_API);
Expand Down Expand Up @@ -60,12 +64,12 @@ export class EnvironmentSettings {

// Default to 'ADMIN_MANAGEMENT_API'
if (!useApi || useApi == this.API.ADMIN_MANAGEMENT_API) {
apiBaseUrl = this.env?.fam_admin_management_api_base_url.value
|| 'http://localhost:8001'; // local api
}
else {
apiBaseUrl = this.env?.fam_api_base_url.value
|| 'http://localhost:8000';
apiBaseUrl =
this.env?.fam_admin_management_api_base_url.value ||
'http://localhost:8001'; // local api
} else {
apiBaseUrl =
this.env?.fam_api_base_url.value || 'http://localhost:8000';
}
return apiBaseUrl;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/FamLoginUserState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface FamLoginUser {
idpProvider?: string; // from ID Token's ['identities']['providerName'] attribute.
authToken?: CognitoUserSession; // original JWT token from AWS Cognito (ID && Access Tokens).
accesses?: FamAuthGrantDto[]; // admin privileges retrieved from backend.
organization?: string;
}

const state = ref({
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/tests/Landing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,19 @@ describe('Landing', () => {
await button.trigger('click');
expect(loginSpy).toHaveBeenCalled();
});
it('should render BCeID button and be disabled', async () => {
it('should render BCeID button and be enabled', async () => {
const button = wrapper.get('#login-bceid-button');
expect(button.classes()).toEqual(
expect.arrayContaining(['landing-button'])
);
expect(button.html().includes('Login with BCeID')).toBe(true);
expect(button.attributes()).toHaveProperty('disabled');
expect(button.attributes()).not.toHaveProperty('disabled');
});
it('should button Login with BCEID be clicked', async () => {
const button = wrapper.get('#login-bceid-button');
const loginSpy = vi.spyOn(AuthService, 'loginBceid');
await button.trigger('click');
expect(loginSpy).toHaveBeenCalled();
});
it('should render image', () => {
const img = wrapper.findAll('.landing-img');
Expand Down
22 changes: 11 additions & 11 deletions infrastructure/server/oidc_clients_fam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ resource "aws_cognito_user_pool_client" "fam_console_oidc_client" {
allowed_oauth_flows = ["code"]
allowed_oauth_flows_user_pool_client = "true"
allowed_oauth_scopes = ["openid", "profile", "email"]
callback_urls = "${concat(var.fam_callback_urls,
[
"${aws_api_gateway_deployment.fam_api_gateway_deployment.invoke_url}/docs/oauth2-redirect",
"${aws_api_gateway_stage.admin_management_api_gateway_stage.invoke_url}/docs/oauth2-redirect"
]
)}"
logout_urls = var.fam_logout_urls
callback_urls = (concat(var.fam_callback_urls,
[
"${aws_api_gateway_deployment.fam_api_gateway_deployment.invoke_url}/docs/oauth2-redirect",
"${aws_api_gateway_stage.admin_management_api_gateway_stage.invoke_url}/docs/oauth2-redirect"
]
))
logout_urls = var.fam_logout_urls
enable_propagate_additional_user_context_data = "false"
enable_token_revocation = "true"
explicit_auth_flows = ["ALLOW_REFRESH_TOKEN_AUTH"]
id_token_validity = "60"
name = "fam_console"
prevent_user_existence_errors = "ENABLED"
read_attributes = "${concat(var.minimum_oidc_attribute_list, ["custom:idp_display_name", "email"])}"
read_attributes = concat(var.minimum_oidc_attribute_list, ["custom:idp_display_name", "email", "custom:idp_business_id", "custom:idp_business_name"])
basilv marked this conversation as resolved.
Show resolved Hide resolved
refresh_token_validity = "24"
supported_identity_providers = [ var.fam_console_idp_name ]
supported_identity_providers = [var.fam_console_idp_name, var.fam_console_idp_name_bceid]
basilv marked this conversation as resolved.
Show resolved Hide resolved

token_validity_units {
access_token = "minutes"
Expand All @@ -27,7 +27,7 @@ resource "aws_cognito_user_pool_client" "fam_console_oidc_client" {
}

user_pool_id = aws_cognito_user_pool.fam_user_pool.id
write_attributes = "${concat(var.minimum_oidc_attribute_list, ["custom:idp_display_name", "email"])}"
write_attributes = concat(var.minimum_oidc_attribute_list, ["custom:idp_display_name", "email", "custom:idp_business_id", "custom:idp_business_name"])

depends_on = [
aws_cognito_identity_provider.dev_idir_oidc_provider,
Expand Down Expand Up @@ -56,4 +56,4 @@ resource "aws_secretsmanager_secret" "fam_oidc_client_id_secret" {
resource "aws_secretsmanager_secret_version" "fam_oidc_client_id_secret_version" {
secret_id = aws_secretsmanager_secret.fam_oidc_client_id_secret.id
secret_string = aws_cognito_user_pool_client.fam_console_oidc_client.id
}
}
5 changes: 5 additions & 0 deletions infrastructure/server/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ output "target_env" {
output "fam_console_idp_name" {
description = "Identifies which version of IDIR to use (DEV, TEST, or PROD)"
value = var.fam_console_idp_name
}

output "fam_console_idp_name_bceid" {
description = "Identifies which version of BUSINESS BCEID to use (DEV, TEST, or PROD)"
value = var.fam_console_idp_name_bceid
}
5 changes: 5 additions & 0 deletions infrastructure/server/variables_provided.tf
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ variable "fam_console_idp_name" {
type = string
}

variable "fam_console_idp_name_bceid" {
description = "Identifies which version of BUSINESS BCEID to use (DEV, TEST, or PROD)"
type = string
}

variable "minimum_oidc_attribute_list" {
description = "Required fields for FAM clients to be able to read and write"
type = list(string)
Expand Down
Loading
Loading