Skip to content

Commit

Permalink
adding documentation for developers
Browse files Browse the repository at this point in the history
Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed Mar 4, 2024
1 parent 47e6797 commit 7598f28
Show file tree
Hide file tree
Showing 4 changed files with 776 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a temporary holding area for developer docs and should be removed when we find a proper home for this documentation.
134 changes: 134 additions & 0 deletions docs/chefs-identity-provider-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# CHEFS Identity Provider

Within the CHEFs application a user's identity provider determines a lot of their access within CHEFs. Keep in mind, this discussion is not on an individual form, this is what menu items, what navigation they have at the application level.

A User's Identity Provider (IDP) is who vouches for them. In a simplified manner: they provide a username and password (generally) and an Identity Provder verifies them and they end up with a token. Currently for CHEFs we have 3 Identity Providers: `IDIR`, `BCeID Basic` and `BCeID Business`. `IDIR` is for employees/contractors on the BC Government. In CHEFs, the `IDIR` Identity Provider allows for greater power within CHEFs; as far as the CHEFs application is concerned IDIR is the `primary` Identity Provider.

Previously, all IDP logic was hardcoded within the frontend code and was difficult to change and maintain.

**Example pseudocode:**

```
if user has idp === 'IDIR' then
enable create forms button
```

By removing the hardcode, we can add in new IDPs and redefine which IDP is the `primary`. This opens up CHEFs for installations in non-BC Government environments.

## Identity Provider Table
Columns are added to the Identity Provider table to support runtime configuration.

* `primary`: boolean, which IDP is the highest level access (currently IDIR)
* `login`: boolean, if this IDP should appear as a login option (Public does not)
* `permissions`: string array, what permissions within CHEFS (not forms) does this IDP have
* `roles`: string array, what Form Roles does this IDP have (designer, owner, submitter, etc)
* `tokenmap`: json blob. this contains the mapping of IDP token fields to userInfo fields.
* `extra`: json blob. this is where non-standard configuration goes. we don't want a column for everything.

### Application Permissions

We have removed this hardcoded dependency and create a set of Application Permissions to replace `if user has idp` logic. We can now use `if user has application permission`. Application Permissions are assigned to one or more IDPs.

```
VIEWS_FORM_STEPPER: 'views_form_stepper',
VIEWS_ADMIN: 'views_admin',
VIEWS_FILE_DOWNLOAD: 'views_file_download',
VIEWS_FORM_EMAILS: 'views_form_emails',
VIEWS_FORM_EXPORT: 'views_form_export',
VIEWS_FORM_MANAGE: 'views_form_manage',
VIEWS_FORM_PREVIEW: 'views_form_preview',
VIEWS_FORM_SUBMISSIONS: 'views_form_submissions',
VIEWS_FORM_TEAMS: 'views_form_teamS',
VIEWS_FORM_VIEW: 'views_form_view',
VIEWS_USER_SUBMISSIONS: 'views_user_submissions',
```

The application permissions will enable/restrict different sections of the CHEFs application.

### Form Roles

Identity Provider also sets the scope of what roles a user can be assigned to an individual form. This was hardcoded and is now part of the Identity Provider configuration. These roles can be assigned to one or more IDPs.

```
OWNER: 'owner',
TEAM_MANAGER: 'team_manager',
FORM_DESIGNER: 'form_designer',
SUBMISSION_REVIEWER: 'submission_reviewer',
FORM_SUBMITTER: 'form_submitter',
```

### Extra
This is a `json` field with no predetermined structure. For BC Gov, we use it for extra functionality for the BCeID IDPs.

There are UX "enhancements" (frontend) and user search restrictions (server side) that were hardcoded, so now moved into this `json`. Any use of `extra` should assume that data fields may not exist or have null values.

Currently, `IDIR` has no data in `extra`.

```
{
formAccessSettings: 'idim',
addTeamMemberSearch: {
text: {
minLength: 6,
message: 'trans.manageSubmissionUsers.searchInputLength',
},
email: {
exact: true,
message: 'trans.manageSubmissionUsers.exactBCEIDSearch',
},
},
userSearch: {
filters: [
{ name: 'filterIdpUserId', param: 'idpUserId', required: 0 },
{ name: 'filterIdpCode', param: 'idpCode', required: 0 },
{ name: 'filterUsername', param: 'username', required: 2, exact: true },
{ name: 'filterFullName', param: 'fullName', required: 0 },
{ name: 'filterFirstName', param: 'firstName', required: 0 },
{ name: 'filterLastName', param: 'lastName', required: 0 },
{ name: 'filterEmail', param: 'email', required: 2, exact: true },
{ name: 'filterSearch', param: 'search', required: 0 },
],
detail: 'Could not retrieve BCeID users. Invalid options provided.'
}
}
```

### Tokenmap
As part of the transistion to a new managed Keycloak realm, we lose the ability to do mapping of Identity Provider attributes to tokens. We do expect our User Information to be standardized and independent of the IDP, so we need to to the mapping ourselves.

The `tokenmap` is a `json` blob that is effectively a `userInfo` property name mapped to a `token` attribute. Each Identity Provider must provide a mapping so we can build out our `userInfo` object (our current user).

```
// userInfo.property: token attribute
{
idpUserId: 'bceid_user_guid',
keycloakId: 'bceid_user_guid',
username: 'bceid_username',
firstName: null,
lastName: null,
fullName: 'name',
email: 'email',
idp: 'identity_provider',
}
```

Note that the `keycloakId` is a GUID and the standard realm does not provide the data as a true GUID, so we need to format it as we build out our `userInfo` object.

### code and idp

Each Identity Provider has a `code` and an `idp`. The `code` never changes and is the `id` and used for referential integrity. Previously, `code` and `idp` were exactly the same. Now that we no longer control the keycloak realm, the actual `idp` values have changed (for `bceid`).

The `idp` fields represents the name if the Identity Provider as found in Keycloak and as returned in the tokens. Within the frontend code, this value is used for idp `hint` - let Keycloak know which IDP the user wished to use for sign in.

The code (both server and frontend) is confusing since `code` and `idp` fields were used interchangeably as the values always matched. `IDIR` still does. In the userInfo/currentUser object `idp` property is actually `code`. Sigh. Added an `idpHint` property but this should be changed to frontend and backend are consistent as are the property/fields names. In the frontend Identity Provider `idp` is `hint` or `idpHint`.

Basically, be aware and cautious with `code`, `idp`, `hint` and `idpHint` until this is addressed.

## Frontend - idpStore
When the application is loaded, we query and store the Identity Providers. This can be found in `frontend/store/identityProviders.js`.

This has helper methods for building the login buttons, getting login hints, the primary IDP and getting data from `extra`. All access to the cached IDP data should come through this store.

## Backend - IdpService
Logic for new Identity Provider fields encapsulated in `components/idpService.js`. The queries and logic for parsing the token (use `tokenmap` field to transform token to userInfo). Also, `userSearch` is here as BCeID has specific requirements that are contained in the `extra` field.

232 changes: 232 additions & 0 deletions docs/chefs-sso-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# CHEFS Single Sign-On (Keycloak Standard Realm)

## History
Current state of OIDC sign in is using a custom Keycloak realm, managed by the CHEFs team. This realm uses Identity Providers for: IDIR, BCeID Basic and BCeID Business.

The custom Keycloak realm allows the CHEFs team complete control over the shape of tokens using Client Scopes and custom mappers.

Both the server/backend and the frontend have their own service clients: `chefs` and `chefs-frontend` respectively. User sign in through the UX/frontend using the `chefs-frontend` client. This client uses the a `chefs` scope to include security (roles) from the `chefs` client. Basically, the `chefs` client is responsible for security and the `chefs-frontend` allows getting a token through the browser.

The server based client (`chefs`) requires a `clientId` and `clientSecret` to connect and perform its security duties. Obviously a frontend client cannot be configured with a secret so that's where the two clients came in.


```
"frontend": {
...
"keycloak": {
"clientId": "chefs-frontend",
"realm": "chefs",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth"
}
},
"server": {
...
"keycloak": {
"clientId": "chefs",
"clientSecret": "...",
"publicKey": "...",
"realm": "chefs",
"serverUrl": "https://dev.loginproxy.gov.bc.ca/auth"
},
...
},
```

When a user would sign in, they would get a token like:

```
{
"exp": 1709164869,
"iat": 1709164569,
"auth_time": 1709164569,
"jti": "4c2fbf8c-518c-484e-8b99-6fc36c9ba12f",
"iss": "https://dev.loginproxy.gov.bc.ca/auth/realms/chefs",
"aud": "chefs",
"sub": "5c3e4a62-974b-4c81-ade5-3f2587d5363c",
"typ": "Bearer",
"azp": "chefs-frontend",
"nonce": "ba7da2cb-fcdf-4146-88b3-cae8e775a891",
"session_state": "6209cc93-9f99-466b-8d15-72f7f6bbc266",
"resource_access": {
"chefs": {
"roles": [
"user"
]
}
},
"scope": "openid chefs",
"sid": "6209cc93-9f99-466b-8d15-72f7f6bbc266",
"identity_provider": "bceid-basic",
"idp_username": "jason.sherman",
"name": "Jason Sherman",
"idp_userid": "22D34CC4510D4943A53362BDECD676C6",
"preferred_username": "22d34cc4510d4943a53362bdecd676c6@bceidbasic",
"given_name": "Jason Sherman",
"email": "[email protected]"
}
```

Note: the `aud`/`audience` is `chefs` even though the client is `chefs-frontend`. And that the `scope` includes `chefs` and also the `resource_access` is qualified by `chefs`.

The ability for CHEFs to manage our own Keycloak realm allows us to add the scope `chefs` to our `chefs-frontend` client and get data from the `chefs` client included in that token. This also allows the `chefs` client to verify and validate this token.

### User role

The user role is added to each user that signs in to the realm. No matter which Identity Provider is used, Keycloak will add a `chefs` user role to that user. This ends up in `resources_access:chefs:roles`.

## Standard realm limitations

Moving to the BC Government standard realm will allow CHEFs to use Single Sign-on but will take control over the shape of the token and they types of service clients we can create. This removes our ability to add custom token mappers for each Identity Provider, use custom scopes and removes auto-assignment of roles.


## Standard realm changes

Most significantly, we only use a single client: `chefs-frontend`. The type of client is changed to `Public` and is for browser logins only. This requires no client secret data to be stored or passed through to the frontend.

There is no need for a backend/server client, but we need to verify the token on each request. And this can be done by asking the OIDC server to verify using JSON Web Key Set (JWKS). So we need configuration to set up the verification.

### SSO Integration Requests

To make requests, and to manage the clients: [Common Hosted Single Sign-On (CSS) Console](https://bcgov.github.io/sso-requests)


**Example SSO Integration Request**

```
Associated Team:
Coco Team
Client Protocol:
OpenID Connect
Client Type:
Public
Usecase:
Browser Login
Project Name:
chefs-frontend
Primary End Users:
People living in BC, People doing business/travel in BC, BC Gov Employees, Other: public - unauthenticated
Identity Providers Required:
IDIR, Basic BCeID, Business BCeID
Dev Redirect URIs:
https://chefs-dev.apps.silver.devops.gov.bc.ca/*
https://chefs-fider.apps.silver.devops.gov.bc.ca/*
https://dev.loginproxy.gov.bc.ca/*
Test Redirect URIs:
https://chefs-fider.apps.silver.devops.gov.bc.ca/*
https://chefs-test.apps.silver.devops.gov.bc.ca/*
https://test.loginproxy.gov.bc.ca/*
Prod Redirect URIs:
https://chefs-fider.apps.silver.devops.gov.bc.ca/*
https://submit.digital.gov.bc.ca/app
```

** IMPORTANT** the client id will not be `chefs-frontend`, but will have some numerical suffix for each environment is deployed. Ex. `chefs-frontend-5299` for development.

#### Admin role
This console will allow us to create `admin` role and then assign that role to users who have signed in using our client. Fairly similar process to what we have now (except we cannot assign by adding a user to a group).

### Identity Providers
Although we have the same identity providers: `IDIR`, `BCeID Basic` and `BCeID Business`, they are named differently. This means the values in tokens for `identity_provider` attribute and used as `idpHints` are different.

In our custom realm: `idir`, `bceid-basic` and `bceid-business`.

In standard realm: `idir`, `bceidbasic` and `bceidbusiness`.

We address this in our IdentityProvider table via `code` and `idp` where `idp` is the Keycloak Identity provider name.


### Token Changes

Since we lose the ability to add custom mappers and the tokens are different for each Identity Provider.

For instance, in each IDP we would map an attribute (`idir_username`, `bceid_username`) that would end up in the token as `idp_username`. So the token would be consistent. So, in the frontend and token parsing is inconsistent as we lose our `idp_XXX` fields. We handle this in the server as we build our user objects by reading a configuration that maps token attributes to user attribues.

**NOTE** maybe we should place similar logic in the frontend. We do have the IDP configuration cached so we can use that to write a parsing function.

Summary:
1. `identity_provider` attribute values have changed
2. `resource_access` no longer supplied, replace with a similar list of roles: `client_roles`
3. `idp_XXX` attributes no longer exist, each IDP has a unique set of attributes. There is overlap on some attributes.


### CHEFs Configuration

Configuration for the frontend does not change signifcantly (nor does the actual javascript/Vue code to interact with the library). We do need to add in a `logoutUrl`.

However the server configuration changes significantly; as does the code base.

**Example configuration**

```
"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"
}
},
"server": {
...
"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"
},
...
},
```

Note that the configuration block key has changed from `keycloak` to `oidc`. This is mainly to allow two completely different CHEFs instances running side by side in our development namespace. As all instances share the same config maps/secrets, we need to deploy a new config map for this transition.

The server configuration now uses the frontend `clientId` as the `audience`. We expect the token to come from a particular issuer for a particular client.

**IMPORTANT** unclear if verifying the `audience/clientId` will allow true single sign-on. Will have to consult with the SSO team and maybe loosen our verify call to only check token age and issuer.

#### Logout URL

The addition of the logout url is to support logging out from Siteminder and Keycloak. Note that the configuration contains only part of the complete logout url as we need to build the redirect url at runtime and add in a `client_id`.

See note [here](https://github.com/bcgov/keycloak-example-apps/blob/4fdf10494dea8b14d460c2d4a8648f0fdccb965c/examples/oidc/public/vue/src/services/keycloak.js#L36).


### OIDC Config Map
Add a new OIDC Config map (no differentition for frontend/server as it is the same client).

```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'
```

### Backend code changes

Significant changes to server/backend code. Most notably we remove `keycloak-connect` library. Keycloak keeps threatening to deprecate this library, so good to get rid of it. However, it did provide a lot of useful middleware that we've had to replicate.

Most logic is found in `components/jwtService.js` including the `protect` middleware. Changes to the token and how we map to a user are found in `components/idpService.js`.


### Frontend code changes

Basically the frontend remains the same as we continue to use the same library: `keycloak-js`.

The `init` is slightly different as we move to a `public` client, we need to specify that we want to use `pkceMethod`:

```
init: { pkceMethod: 'S256', checkLoginIframe: false, onLoad: 'check-sso' },
```

Changes to the token mean we change how we determine roles. We no longer qualify by resource (`chefs`). and we get the data from `client_roles`.

Since we added the `logoutUrl`, the logout method has changed too. `logoutUrl` is optional, which will make it easier for non-BC installations. See the auth store (`store/auth.js`).


Loading

0 comments on commit 7598f28

Please sign in to comment.