Skip to content

Commit

Permalink
feature: sso auth (#694)
Browse files Browse the repository at this point in the history
* Add routes and session for staff sso oauth

* Add account view

* Test OAuth config

* Complete adding tests and refactor into plugin

* Add manifests for CF deploy

* Fix docker images

* Rename docker images for consistency

* Move to single root manifest with app name var

* Attempt at magical substitution

* Another attempt at magical substitution

* Manual substitution

* Prevent eager substitution in sed command

* Force https for redirect url

* Force https for redirect url - debug

* Generate auth cookie password

* Show logout link in header

* Use service URL var to determine force https config

* Remove TODO

* Enforce login for form pages if SSO config set

* Remove account view

* Rename vars to be less specific

* Update README

* Update components diagram

* Reformat README

* Fix for undefined request

* Use @hapi/bell as it is more up-to-date

* Move login redirect logic to engine plugin

* Remove CloudFoundry manifest

* Prefer official hapi library and use secure cookie

* Remove unnecessary comment

* Reset session on logout

* Run all tests O_o
  • Loading branch information
froddd authored Nov 29, 2021
1 parent d661f26 commit e7dbefe
Show file tree
Hide file tree
Showing 18 changed files with 473 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docs/components-diagram.drawio

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/components-diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 27 additions & 21 deletions runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,33 @@ To symlink an external .env file, for example inside a [Keybase](https://keybase
`symlink-config` accepts two variables, ENV_LOC and LINK_TO. If the file location is not passed in, you will be prompted for a location.
LINK_TO is optional, it defaults to `./${PROJECT_DIR}`.

| name | description | required | default | valid | notes |
| ------------------ | ------------------------------------- | :------: | ------- | :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: |
| NODE_ENV | Node environment | no | | development,test,production | |
| PORT | Port number | no | 3009 | | |
| OS_KEY | Ordnance Survey | no | | | For address lookup by postcode |
| PAY_API_KEY | Pay api key | yes | | | |
| PAY_RETURN_URL | Pay return url | yes | | | For GOV.UK Pay to redirect back to our service |
| PAY_API_URL | Pay api url | yes | | | |
| NOTIFY_TEMPLATE_ID | Notify api key | yes | | | Template ID required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. |
| NOTIFY_API_KEY | Notify api key | yes | | | API KEY required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. |
| GTM_ID_1 | Google Tag Manager ID 1 | no | | | |
| GTM_ID_2 | Google Tag Manager ID 2 | no | | | |
| MATOMO_URL | URL of Matomo | no | | | |
| MATOMO_ID | ID of Matomo site | no | | | |
| SSL_KEY | SSL Key | no | | | |
| SSL_CERT | SSL Certificate | no | | | |
| PREVIEW_MODE | Preview mode | no | false | | This should only be used in a dev or testing environment. Setting true will allow POST requests from the designer to add or mutate forms. |
| LOG_LEVEL | Log level | no | debug | trace,debug,info,error | |
| PRIVACY_POLICY_URL | The url used in footer's privacy link | no | # | | |
| API_ENV | Switch for API keys | no | | test,production | If the JSON file supplies test and live API keys, this is used to switch between which key which needs to be used |
| PHASE_TAG | Tag to use for phase banner | no | beta | alpha, beta, empty string | |
| name | description | required | default | valid | notes |
| ----------------------- | ------------------------------------- | :---------------------: | ------- | :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: |
| NODE_ENV | Node environment | no | | development,test,production | |
| PORT | Port number | no | 3009 | | |
| OS_KEY | Ordnance Survey | no | | | For address lookup by postcode |
| PAY_API_KEY | Pay api key | yes | | | |
| PAY_RETURN_URL | Pay return url | yes | | | For GOV.UK Pay to redirect back to our service |
| PAY_API_URL | Pay api url | yes | | | |
| NOTIFY_TEMPLATE_ID | Notify api key | yes | | | Template ID required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. |
| NOTIFY_API_KEY | Notify api key | yes | | | API KEY required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. |
| GTM_ID_1 | Google Tag Manager ID 1 | no | | | |
| GTM_ID_2 | Google Tag Manager ID 2 | no | | | |
| MATOMO_URL | URL of Matomo | no | | | |
| MATOMO_ID | ID of Matomo site | no | | | |
| SSL_KEY | SSL Key | no | | | |
| SSL_CERT | SSL Certificate | no | | | |
| PREVIEW_MODE | Preview mode | no | false | | This should only be used in a dev or testing environment. Setting true will allow POST requests from the designer to add or mutate forms. |
| LOG_LEVEL | Log level | no | debug | trace,debug,info,error | |
| PRIVACY_POLICY_URL | The url used in footer's privacy link | no | # | | |
| API_ENV | Switch for API keys | no | | test,production | If the JSON file supplies test and live API keys, this is used to switch between which key which needs to be used |
| PHASE_TAG | Tag to use for phase banner | no | beta | alpha, beta, empty string | |
| AUTH_ENABLED | Enable auth for all form pages | no | false | | |
| AUTH_CLIENT_ID | oAuth client ID | If AUTH_ENABLED is true | | | |
| AUTH_CLIENT_SECRET | oAuth client secret | If AUTH_ENABLED is true | | | |
| AUTH_CLIENT_AUTH_URL | oAuth client authorise endpoint | If AUTH_ENABLED is true | | | |
| AUTH_CLIENT_TOKEN_URL | oAuth client token endpoint | If AUTH_ENABLED is true | | | |
| AUTH_CLIENT_PROFILE_URL | oAuth client user profile endpoint | If AUTH_ENABLED is true | | | |

# Testing

Expand Down
2 changes: 2 additions & 0 deletions runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@babel/runtime": "7.10.4",
"@hapi/bell": "^12.3.0",
"@hapi/boom": "9.1.0",
"@hapi/catbox": "11.1.0",
"@hapi/catbox-memory": "5.0.0",
"@hapi/catbox-redis": "5.0.5",
"@hapi/code": "8.0.1",
"@hapi/cookie": "^11.0.2",
"@hapi/crumb": "8.0.0",
"@hapi/hapi": "^20.0.3",
"@hapi/inert": "5.2.2",
Expand Down
27 changes: 27 additions & 0 deletions runner/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,27 @@ const schema = Joi.object({
lastCommit: Joi.string().default("undefined"),
lastTag: Joi.string().default("undefined"),
apiEnv: Joi.string().allow("test", "production", "").optional(),
authEnabled: Joi.boolean().optional(),
authClientId: Joi.string().when("authEnabled", {
then: Joi.required(),
otherwise: Joi.optional(),
}),
authClientSecret: Joi.string().when("authEnabled", {
then: Joi.required(),
otherwise: Joi.optional(),
}),
authClientAuthUrl: Joi.string().when("authEnabled", {
then: Joi.required(),
otherwise: Joi.optional(),
}),
authClientTokenUrl: Joi.string().when("authEnabled", {
then: Joi.required(),
otherwise: Joi.optional(),
}),
authClientProfileUrl: Joi.string().when("authEnabled", {
then: Joi.required(),
otherwise: Joi.optional(),
}),
});

export function buildConfig() {
Expand Down Expand Up @@ -118,6 +139,12 @@ export function buildConfig() {
lastCommit: process.env.LAST_COMMIT || process.env.LAST_COMMIT_GH,
lastTag: process.env.LAST_TAG || process.env.LAST_TAG_GH,
apiEnv: process.env.API_ENV,
authEnabled: process.env.AUTH_ENABLED,
authClientId: process.env.AUTH_CLIENT_ID,
authClientSecret: process.env.AUTH_CLIENT_SECRET,
authClientAuthUrl: process.env.AUTH_CLIENT_AUTH_URL,
authClientTokenUrl: process.env.AUTH_CLIENT_TOKEN_URL,
authClientProfileUrl: process.env.AUTH_CLIENT_PROFILE_URL,
};

// Validate config
Expand Down
16 changes: 13 additions & 3 deletions runner/src/server/forms/test.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
{
"startPage": "/uk-passport",
"startPage": "/start",
"pages": [
{
"title": "Start",
"path": "/start",
"components": [],
"next": [
{
"path": "/uk-passport"
}
],
"controller": "./pages/start.js"
},
{
"path": "/uk-passport",
"components": [
Expand Down Expand Up @@ -454,8 +465,7 @@
}
],
"phaseBanner": {},
"fees": [
],
"fees": [],
"payApiKey": "",
"outputs": [
{
Expand Down
6 changes: 4 additions & 2 deletions runner/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ import { configureBlankiePlugin } from "./plugins/blankie";
import { configureCrumbPlugin } from "./plugins/crumb";
import pluginLocale from "./plugins/locale";
import pluginSession from "./plugins/session";
import pluginAuth from "./plugins/auth";
import pluginViews from "./plugins/views";
import pluginApplicationStatus from "./plugins/applicationStatus";
import pluginRouter from "./plugins/router";
import pluginErrorPages from "./plugins/errorPages";
import pluginLogging from "./plugins/logging";
import pluginPulse from "./plugins/pulse";
import {
AddressService,
CacheService,
catboxProvider,
EmailService,
NotifyService,
PayService,
StatusService,
UploadService,
WebhookService,
AddressService,
StatusService,
} from "./services";
import { HapiRequest, HapiResponseToolkit, RouteConfig } from "./types";
import getRequestInfo from "./utils/getRequestInfo";
Expand Down Expand Up @@ -89,6 +90,7 @@ async function createServer(routeConfig: RouteConfig) {
await server.register(configureCrumbPlugin(config, routeConfig));
await server.register(pluginLogging);
await server.register(Schmervice);
await server.register(pluginAuth);

server.registerService([
CacheService,
Expand Down
83 changes: 83 additions & 0 deletions runner/src/server/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import AuthCookie from "@hapi/cookie";
import Bell from "@hapi/bell";

import config from "server/config";
import { HapiRequest, HapiResponseToolkit } from "server/types";
import { redirectTo } from "server/plugins/engine";
import generateCookiePassword from "server/utils/generateCookiePassword";

export const shouldLogin = (request: HapiRequest) =>
config.authEnabled && !request.auth.isAuthenticated;

export default {
plugin: {
name: "auth",
register: async (server) => {
if (!config.authEnabled) {
return;
}

await server.register(AuthCookie);
await server.register(Bell);

server.auth.strategy("session", "cookie", {
cookie: {
name: "auth",
password: config.sessionCookiePassword || generateCookiePassword(),
isSecure: true,
},
});

server.auth.strategy("oauth", "bell", {
provider: {
name: "oauth",
protocol: "oauth2",
auth: config.authClientAuthUrl,
token: config.authClientTokenUrl,
scope: ["read write"],
profile: async (credentials, _params, get) => {
const { email, first_name, last_name, user_id } = await get(
config.authClientProfileUrl
);
credentials.profile = { email, first_name, last_name, user_id };
},
},
password: config.sessionCookiePassword || generateCookiePassword(),
clientId: config.authClientId,
clientSecret: config.authClientSecret,
forceHttps: config.serviceUrl.startsWith("https"),
});

server.auth.default({ strategy: "session", mode: "try" });

server.route({
method: ["GET", "POST"],
path: "/login",
config: {
auth: "oauth",
handler: (request: HapiRequest, h: HapiResponseToolkit) => {
if (request.auth.isAuthenticated) {
request.cookieAuth.set(request.auth.credentials.profile);
const returnUrl =
request.auth.credentials.query?.returnUrl || "/";
return redirectTo(request, h, returnUrl);
}

return h.response(JSON.stringify(request));
},
},
});

server.route({
method: "get",
path: "/logout",
handler: async (request: HapiRequest, h: HapiResponseToolkit) => {
request.cookieAuth.clear();
request.yar.reset();

return redirectTo(request, h, "/");
},
});
},
},
};
Loading

0 comments on commit e7dbefe

Please sign in to comment.