Skip to content

Commit 93b92f1

Browse files
FS-4794 enable SSO active directory login feature in designer side (#93)
* FS-4794: adding sso auth into designer * FS-4794: adding co - pilot configs * e2e fix for designer * ignore login if there is no auth also remove sign out button from the login screen adding common footer * adding fixes for e2e
1 parent 60c6732 commit 93b92f1

File tree

17 files changed

+525
-29
lines changed

17 files changed

+525
-29
lines changed

.run/DESIGNER No AUTH.run.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<envs>
1111
<env name="ACCESSIBILITY_STATEMENT_URL" value="http://localhost:3008/accessibility_statement" />
1212
<env name="ALLOW_USER_TEMPLATES" value="true" />
13+
<env name="AUTH_COOKIE_NAME" value="fsd_user_token" />
14+
<env name="AUTH_ENABLED" value="false" />
1315
<env name="AWS_ACCESS_KEY_ID" value="FSDIOSFODNN7EXAMPLE" />
1416
<env name="AWS_BUCKET_NAME" value="fsd-bucket" />
1517
<env name="AWS_DEFAULT_REGION" value="eu-west-2" />
@@ -21,13 +23,11 @@
2123
<env name="COOKIE_POLICY_URL" value="http://localhost:3008/cookie_policy" />
2224
<env name="ELIGIBILITY_RESULT_URL" value="http://localhost:3008/eligibility-result" />
2325
<env name="FEEDBACK_LINK" value="http://localhost:3008/feedback" />
24-
<env name="JWT_AUTH_COOKIE_NAME" value="fsd_user_token" />
25-
<env name="JWT_AUTH_ENABLED" value="false" />
26-
<env name="JWT_REDIRECT_TO_AUTHENTICATION_URL" value="http://localhost:3004/sessions/sign-out" />
2726
<env name="LOG_LEVEL" value="debug" />
2827
<env name="LOGOUT_URL" value="http://localhost:3004/sessions/sign-out" />
2928
<env name="MULTIFUND_URL" value="http://localhost:3008/account" />
3029
<env name="PRIVACY_POLICY_URL" value="http://localhost:3008/privacy" />
30+
<env name="REDIRECT_TO_AUTHENTICATION_URL" value="http://localhost:3004/sessions/sign-out" />
3131
<env name="RSA256_PUBLIC_KEY_BASE64" value="&quot;LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZU1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTUFEQ0JpQUtCZ0hHYnRGMXlWR1crckNBRk9JZGFrVVZ3Q2Z1dgp4SEUzOGxFL2kwS1dwTXdkU0haRkZMWW5IakJWT09oMTVFaWl6WXphNEZUSlRNdkwyRTRRckxwcVlqNktFNnR2CkhyaHlQL041ZnlwU3p0OHZDajlzcFo4KzBrRnVjVzl6eU1rUHVEaXNZdG1rV0dkeEJta2QzZ3RZcDNtT0k1M1YKVkRnS2J0b0lGVTNzSWs1TkFnTUJBQUU9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ\=\=&quot;" />
3232
<env name="SERVICE_START_PAGE" value="http://localhost:3008/account" />
3333
</envs>
@@ -44,4 +44,4 @@
4444
</option>
4545
</method>
4646
</configuration>
47-
</component>
47+
</component>

copilot/fsd-form-designer-adapter/manifest.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ variables:
5555
CHOKIDAR_USEPOLLING: true
5656
PREVIEW_URL: "https://forms.${COPILOT_ENVIRONMENT_NAME}.access-funding.test.levellingup.gov.uk"
5757
PUBLISH_URL: "http://fsd-form-runner-adapter:3009"
58+
AUTH_SERVICE_URL: "https://authenticator.${COPILOT_ENVIRONMENT_NAME}.access-funding.test.levellingup.gov.uk"
59+
SSO_LOGIN_URL: "/sso/login?return_app=form-designer"
60+
SSO_LOGOUT_URL: "/sessions/sign-out"
61+
AUTH_COOKIE_NAME: "fsd_user_token"
62+
AUTH_ENABLED: true
5863

5964
secrets:
6065
RSA256_PUBLIC_KEY_BASE64: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/RSA256_PUBLIC_KEY_BASE64
@@ -65,6 +70,38 @@ environments:
6570
dev:
6671
count:
6772
spot: 1
73+
sidecars:
74+
nginx:
75+
port: 8087
76+
image:
77+
location: xscys/nginx-sidecar-basic-auth
78+
variables:
79+
FORWARD_PORT: 8080
80+
CLIENT_MAX_BODY_SIZE: 10m
81+
secrets:
82+
BASIC_AUTH_USERNAME: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/BASIC_AUTH_USERNAME
83+
BASIC_AUTH_PASSWORD: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/BASIC_AUTH_PASSWORD
84+
http:
85+
target_container: nginx
86+
healthcheck:
87+
path: /healthcheck
88+
port: 8080
6889
test:
6990
count:
7091
spot: 2
92+
sidecars:
93+
nginx:
94+
port: 8087
95+
image:
96+
location: xscys/nginx-sidecar-basic-auth
97+
variables:
98+
FORWARD_PORT: 8080
99+
CLIENT_MAX_BODY_SIZE: 10m
100+
secrets:
101+
BASIC_AUTH_USERNAME: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/BASIC_AUTH_USERNAME
102+
BASIC_AUTH_PASSWORD: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/BASIC_AUTH_PASSWORD
103+
http:
104+
target_container: nginx
105+
healthcheck:
106+
path: /healthcheck
107+
port: 8080

designer/server/config.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import dotenv from "dotenv";
2+
import joi from "joi";
3+
import {CredentialsOptions} from "aws-sdk/lib/credentials";
4+
import * as AWS from "aws-sdk";
5+
6+
dotenv.config({path: ".env"});
7+
8+
export interface Config {
9+
env: "development" | "test" | "production";
10+
port: number;
11+
previewUrl: string;
12+
publishUrl: string;
13+
persistentBackend: "s3" | "blob" | "preview";
14+
s3Bucket?: string;
15+
logLevel: "trace" | "info" | "debug" | "error";
16+
phase?: "alpha" | "beta";
17+
footerText?: string;
18+
isProd: boolean;
19+
isDev: boolean;
20+
isTest: boolean;
21+
lastCommit: string;
22+
lastTag: string;
23+
sessionTimeout: number;
24+
sessionCookiePassword: string;
25+
awsCredentials?: CredentialsOptions;
26+
authEnabled: boolean,
27+
ssoLoginUrl: string,
28+
ssoLogoutUrl: string,
29+
authServiceUrl: string,
30+
rsa256PublicKeyBase64: string,
31+
authCookieName: string,
32+
}
33+
34+
// server-side storage expiration - defaults to 20 minutes
35+
const sessionSTimeoutInMilliseconds = 20 * 60 * 1000;
36+
37+
// Define config schema
38+
const schema = joi.object({
39+
port: joi.number().default(3000),
40+
env: joi
41+
.string()
42+
.valid("development", "test", "production")
43+
.default("development"),
44+
previewUrl: joi.string(),
45+
publishUrl: joi.string(),
46+
persistentBackend: joi.string().valid("s3", "blob", "preview").optional(),
47+
s3Bucket: joi.string().optional(),
48+
logLevel: joi
49+
.string()
50+
.valid("trace", "info", "debug", "error")
51+
.default("debug"),
52+
phase: joi.string().valid("alpha", "beta").optional(),
53+
footerText: joi.string().optional(),
54+
lastCommit: joi.string().default("undefined"),
55+
lastTag: joi.string().default("undefined"),
56+
sessionTimeout: joi.number().default(sessionSTimeoutInMilliseconds),
57+
sessionCookiePassword: joi.string().optional(),
58+
authEnabled: joi.string().required(),
59+
ssoLoginUrl: joi.string().optional(),
60+
ssoLogoutUrl: joi.string().optional(),
61+
authServiceUrl: joi.string().optional(),
62+
rsa256PublicKeyBase64: joi.string().optional(),
63+
authCookieName: joi.string().optional(),
64+
});
65+
66+
// Build config
67+
const config = {
68+
port: process.env.PORT,
69+
env: process.env.NODE_ENV,
70+
previewUrl: process.env.PREVIEW_URL || "http://localhost:3009",
71+
publishUrl: process.env.PUBLISH_URL || "http://localhost:3009",
72+
persistentBackend: process.env.PERSISTENT_BACKEND || "preview",
73+
s3Bucket: process.env.S3_BUCKET,
74+
logLevel: process.env.LOG_LEVEL || "error",
75+
phase: process.env.PHASE || "alpha",
76+
footerText: process.env.FOOTER_TEXT,
77+
lastCommit: process.env.LAST_COMMIT || process.env.LAST_COMMIT_GH,
78+
lastTag: process.env.LAST_TAG || process.env.LAST_TAG_GH,
79+
sessionTimeout: process.env.SESSION_TIMEOUT,
80+
sessionCookiePassword: process.env.SESSION_COOKIE_PASSWORD,
81+
authEnabled: process.env.AUTH_ENABLED,
82+
ssoLoginUrl: process.env.SSO_LOGIN_URL,
83+
ssoLogoutUrl: process.env.SSO_LOGOUT_URL,
84+
authServiceUrl: process.env.AUTH_SERVICE_URL,
85+
rsa256PublicKeyBase64: process.env.RSA256_PUBLIC_KEY_BASE64,
86+
authCookieName: process.env.AUTH_COOKIE_NAME,
87+
};
88+
89+
// Validate config
90+
const result = schema.validate(
91+
{
92+
...config,
93+
},
94+
{abortEarly: false}
95+
);
96+
97+
// Throw if config is invalid
98+
if (result.error) {
99+
throw new Error(`The server config is invalid. ${result.error.message}`);
100+
}
101+
102+
// Use the joi validated value
103+
const value: Config = result.value;
104+
105+
/**
106+
* TODO:- replace this with a top-level await when upgraded to node 16
107+
*/
108+
async function getAwsConfigCredentials(): Promise<CredentialsOptions | {}> {
109+
return new Promise(function (resolve, reject) {
110+
if (value.persistentBackend === "s3") {
111+
return AWS.config.getCredentials(async function (err) {
112+
if (err) {
113+
console.error("Error getting AWS credentials", err);
114+
reject(err);
115+
} else {
116+
resolve({
117+
// @ts-ignore
118+
accessKeyId: AWS.config.credentials.accessKeyId,
119+
// @ts-ignore
120+
secretAccessKey: AWS.config.credentials.secretAccessKey,
121+
});
122+
}
123+
});
124+
} else {
125+
resolve({});
126+
}
127+
});
128+
}
129+
130+
getAwsConfigCredentials()
131+
.then((awsConfig) => {
132+
// @ts-ignore
133+
value.awsCredentials = awsConfig;
134+
})
135+
.catch((e) => {
136+
throw e;
137+
});
138+
139+
value.isProd = value.env === "production";
140+
value.isDev = !value.isProd;
141+
value.isTest = value.env === "test";
142+
143+
export default value;

designer/server/createServer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {determinePersistenceService} from "../../digital-form-builder/designer/s
1010
import {configureBlankiePlugin} from "../../digital-form-builder/designer/server/plugins/blankie";
1111
import {configureYarPlugin} from "../../digital-form-builder/designer/server/plugins/session";
1212
import {designerPlugin} from "./plugins/DesignerRouteRegister";
13+
import errorHandlerPlugin from "./plugins/ErrorHandlerPlugin";
14+
import authPlugin from "./plugins/AuthPlugin";
1315

1416
const serverOptions = () => {
1517
return {
@@ -40,6 +42,9 @@ const serverOptions = () => {
4042
export async function createServer() {
4143
//@ts-ignore
4244
const server = hapi.server(serverOptions());
45+
await server.register(errorHandlerPlugin);
46+
//@ts-ignore
47+
await server.register(authPlugin);
4348
await server.register(inert);
4449
await server.register(Scooter);
4550
//@ts-ignore
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
2+
import JwtPlugin from "hapi-auth-jwt2"
3+
import {HapiServer} from "../../../digital-form-builder/designer/server/types";
4+
import config from "../config";
5+
6+
export const jwtAuthStrategyName = "jwt_auth";
7+
8+
// rsa256Options()
9+
// Returns configuration options for rsa256 auth strategy
10+
export function rsa256Options(jwtAuthCookieName) {
11+
return {
12+
key: keyFunc,
13+
validate,
14+
verifyOptions: {
15+
algorithms: ["RS256"],
16+
},
17+
urlKey: false,
18+
cookieKey: jwtAuthCookieName,
19+
};
20+
}
21+
22+
// keyFunc returns the key and any additional context required to
23+
// pass to validate the function (below) to validate signature
24+
// this is normally used to look up keys from list in a multi-tenant scenario
25+
const keyFunc = async function (decoded) {
26+
const key = Buffer.from(config.rsa256PublicKeyBase64 ?? "", "base64");
27+
return {key, additional: decoded};
28+
};
29+
30+
// validate()
31+
// Checks validity of user credentials
32+
// @ts-ignore
33+
const validate = async function (decoded, request, h) {
34+
// This runs if the jwt signature is verified
35+
// It must return an object with an 'isValid' boolean property,
36+
// this allows the user to continue if true or raises a 401 if false
37+
const credentials = decoded;
38+
if (request.plugins["hapi-auth-jwt2"]) {
39+
credentials.extraInfo = request.plugins["hapi-auth-jwt2"].extraInfo;
40+
}
41+
if (!decoded.accountId) {
42+
request.logger.error(
43+
"JWT token has no accountID in jwt: " + credentials.extraInfo.toString()
44+
);
45+
return {isValid: false};
46+
} else {
47+
return {isValid: true, credentials};
48+
}
49+
};
50+
51+
export default {
52+
plugin: {
53+
name: "auth",
54+
register: async (server: HapiServer) => {
55+
// @ts-ignore
56+
if (config.authEnabled && config.authEnabled === "true") {
57+
// @ts-ignore
58+
await server.register(JwtPlugin);
59+
console.log(`JWT Authentication Enabled: ${config.authEnabled}`);
60+
console.log(`JWT Authentication cookie name: ${config.rsa256PublicKeyBase64}`);
61+
console.log(`JWT Authentication strategy name: ${jwtAuthStrategyName}`);
62+
server.auth.strategy(jwtAuthStrategyName, "jwt", rsa256Options(config.authCookieName));
63+
} else {
64+
return;
65+
}
66+
},
67+
},
68+
};

0 commit comments

Comments
 (0)