Skip to content

Commit

Permalink
Single sign on functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
BradyMitch committed Oct 17, 2024
1 parent 1531adb commit 1eabd0d
Show file tree
Hide file tree
Showing 28 changed files with 641 additions and 95 deletions.
46 changes: 0 additions & 46 deletions backend/biome.json

This file was deleted.

Binary file added backend/js-core.tgz
Binary file not shown.
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
},
"dependencies": {
"@bcgov/citz-imb-express-utilities": "1.0.0-beta4",
"@bcgov/citz-imb-sso-js-core": "file:./js-core.tgz",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"express": "4.21.0",
"express-rate-limit": "7.4.1",
Expand All @@ -16,6 +18,7 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.3",
"@types/cookie-parser": "1.4.7",
"@types/cors": "2.8.17",
"@types/express": "5.0.0",
"@types/node": "22.7.4",
Expand Down
10 changes: 10 additions & 0 deletions backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const {
MONGO_PASSWORD,
MONGO_DATABASE_NAME,
MONGO_HOST,
SSO_ENVIRONMENT = "dev",
SSO_REALM = "standard",
SSO_PROTOCOL = "openid-connect",
SSO_CLIENT_ID = "",
SSO_CLIENT_SECRET = "",
} = process.env;

// Exported configuration values.
Expand All @@ -21,4 +26,9 @@ export default {
MONGO_PASSWORD,
MONGO_DATABASE_NAME,
MONGO_HOST,
SSO_ENVIRONMENT,
SSO_REALM,
SSO_PROTOCOL,
SSO_CLIENT_ID,
SSO_CLIENT_SECRET,
};
13 changes: 13 additions & 0 deletions backend/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
import cors from "cors";
import express from "express";
import rateLimit from "express-rate-limit";
import cookieParser from "cookie-parser";
import { CORS_OPTIONS, RATE_LIMIT_OPTIONS } from "./config";
import { ENV } from "./config";
import { router } from "./modules/auth";
import { protectedRoute } from "./modules/auth/middleware";
import type { Request, Response } from "express";

const { ENVIRONMENT } = ENV;

Expand All @@ -19,15 +23,24 @@ app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cors(CORS_OPTIONS));
app.use(rateLimit(RATE_LIMIT_OPTIONS));
app.use(cookieParser());
app.set("view engine", "ejs");

// Disabled because it exposes information about the used framework to potential attackers.
app.disable("x-powered-by");

app.use("/auth", router);

// Add express utils middleware.
app.use(expressUtilitiesMiddleware);

// Routing
healthModule(app); // Route (/health)
configModule(app, { ENVIRONMENT }); // Route (/config)

app.use("/test", protectedRoute(["Admin"]), (req: Request, res: Response) => {
console.log("HERE");
res.json({ message: "YES" });
});

export default app;
1 change: 0 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ app.listen(PORT, () => {
try {
// Log server start information.
serverStartupLogs(PORT);
console.log(`API has started, and is listening on ${PORT}`)
} catch (error) {
// Log any error that occurs during the server start.
console.error(error);
Expand Down
5 changes: 5 additions & 0 deletions backend/src/modules/auth/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./login";
export * from "./loginCallback";
export * from "./logout";
export * from "./logoutCallback";
export * from "./token";
32 changes: 32 additions & 0 deletions backend/src/modules/auth/controllers/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Request, Response } from "express";
import { getLoginURL } from "@bcgov/citz-imb-sso-js-core";
import type { SSOEnvironment, SSOProtocol } from "@bcgov/citz-imb-sso-js-core";
import { ENV } from "src/config";
import { errorWrapper } from "@bcgov/citz-imb-express-utilities";

const { SSO_ENVIRONMENT, SSO_REALM, SSO_PROTOCOL, SSO_CLIENT_ID, BACKEND_URL } =
ENV;

export const login = errorWrapper(async (req: Request, res: Response) => {
try {
const redirectURL = getLoginURL({
idpHint: "idir",
clientID: SSO_CLIENT_ID,
redirectURI: `${BACKEND_URL}/auth/login/callback`,
ssoEnvironment: SSO_ENVIRONMENT as SSOEnvironment,
ssoRealm: SSO_REALM,
ssoProtocol: SSO_PROTOCOL as SSOProtocol,
});

// Redirect the user to the SSO login page
res.redirect(redirectURL);
} catch (error) {
res.status(500).json({
success: false,
error:
error instanceof Error
? error.message
: "An unknown error occurred during login.",
});
}
});
56 changes: 56 additions & 0 deletions backend/src/modules/auth/controllers/loginCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Request, Response } from "express";
import { getTokens } from "@bcgov/citz-imb-sso-js-core";
import type { SSOEnvironment, SSOProtocol } from "@bcgov/citz-imb-sso-js-core";
import { ENV } from "src/config";
import { errorWrapper } from "@bcgov/citz-imb-express-utilities";

const { SSO_ENVIRONMENT, SSO_REALM, SSO_PROTOCOL, SSO_CLIENT_ID, SSO_CLIENT_SECRET, BACKEND_URL } =
ENV;

export const loginCallback = errorWrapper(async (req: Request, res: Response) => {
try {
const { code } = req.query;

const tokens = await getTokens({
code: code as string,
clientID: SSO_CLIENT_ID,
clientSecret: SSO_CLIENT_SECRET,
redirectURI: `${BACKEND_URL}/auth/login/callback`,
ssoEnvironment: SSO_ENVIRONMENT as SSOEnvironment,
ssoRealm: SSO_REALM,
ssoProtocol: SSO_PROTOCOL as SSOProtocol,
});

// Sets tokens
res
.cookie("refresh_token", tokens.refresh_token, {
httpOnly: true,
secure: true,
sameSite: "none",
})
.cookie("access_token", tokens.access_token, {
secure: true,
sameSite: "none",
})
.cookie("id_token", tokens.id_token, {
secure: true,
sameSite: "none",
})
.cookie("expires_in", tokens.expires_in, {
secure: true,
sameSite: "none",
})
.cookie("refresh_expires_in", tokens.refresh_expires_in, {
secure: true,
sameSite: "none",
})
.status(200)
.json(tokens);
} catch (error) {
res.status(500).json({
success: false,
error:
error instanceof Error ? error.message : "An unknown error occurred during login callback.",
});
}
});
31 changes: 31 additions & 0 deletions backend/src/modules/auth/controllers/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Request, Response } from "express";
import { getLogoutURL } from "@bcgov/citz-imb-sso-js-core";
import type { SSOEnvironment, SSOProtocol } from "@bcgov/citz-imb-sso-js-core";
import { ENV } from "src/config";
import { errorWrapper } from "@bcgov/citz-imb-express-utilities";

const { SSO_ENVIRONMENT, SSO_REALM, SSO_PROTOCOL, BACKEND_URL } = ENV;

export const logout = errorWrapper(async (req: Request, res: Response) => {
try {
const { id_token } = req.query;

const redirectURL = getLogoutURL({
idToken: id_token as string,
postLogoutRedirectURI: `${BACKEND_URL}/auth/logout/callback`,
ssoEnvironment: SSO_ENVIRONMENT as SSOEnvironment,
ssoProtocol: SSO_PROTOCOL as SSOProtocol,
ssoRealm: SSO_REALM,
});

res.redirect(redirectURL);
} catch (error) {
res.status(500).json({
success: false,
error:
error instanceof Error
? error.message
: "An unknown error occurred during logout.",
});
}
});
20 changes: 20 additions & 0 deletions backend/src/modules/auth/controllers/logoutCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { errorWrapper } from "@bcgov/citz-imb-express-utilities";
import type { Request, Response } from "express";

// This endpoint is only needed because SSO needs to redirect somewhere
// and the desktop application does not have a URL to redirect to.
export const logoutCallback = errorWrapper(
async (req: Request, res: Response) => {
try {
res.status(204).send("Logged out.");
} catch (error) {
res.status(500).json({
success: false,
error:
error instanceof Error
? error.message
: "An unknown error occurred during logout.",
});
}
},
);
47 changes: 47 additions & 0 deletions backend/src/modules/auth/controllers/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Request, Response } from "express";
import { getNewTokens } from "@bcgov/citz-imb-sso-js-core";
import type { SSOEnvironment, SSOProtocol } from "@bcgov/citz-imb-sso-js-core";
import { ENV } from "src/config";
import { errorWrapper } from "@bcgov/citz-imb-express-utilities";

const { SSO_ENVIRONMENT, SSO_REALM, SSO_PROTOCOL, SSO_CLIENT_ID, SSO_CLIENT_SECRET } = ENV;

export const token = errorWrapper(async (req: Request, res: Response) => {
try {
const refresh_token = req.cookies.refresh_token;

if (!refresh_token)
return res.status(401).json({
success: false,
message: "Refresh token is missing. Please log in again.",
});

const tokens = await getNewTokens({
refreshToken: refresh_token as string,
clientID: SSO_CLIENT_ID,
clientSecret: SSO_CLIENT_SECRET,
ssoEnvironment: SSO_ENVIRONMENT as SSOEnvironment,
ssoRealm: SSO_REALM,
ssoProtocol: SSO_PROTOCOL as SSOProtocol,
});

if (!tokens) return res.status(401).json({ success: false, message: "Invalid token." });

// Set token
res
.cookie("access_token", tokens.access_token, {
secure: true,
sameSite: "none",
})
.status(200)
.json(tokens);
} catch (error) {
res.status(500).json({
success: false,
error:
error instanceof Error
? error.message
: "An unknown error occurred while refreshing tokens.",
});
}
});
1 change: 1 addition & 0 deletions backend/src/modules/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as router } from "./router";
Loading

0 comments on commit 1eabd0d

Please sign in to comment.