diff --git a/docker/supervisord-dev.conf b/docker/supervisord-dev.conf
index 7f646e3..3bd0a82 100644
--- a/docker/supervisord-dev.conf
+++ b/docker/supervisord-dev.conf
@@ -15,7 +15,7 @@ startsecs=2
priority=1
[program:auth]
-command=npx nodemon app.js
+command=npx nodemon app.js -e ejs,js,css,html,jpg,png,scss
autostart=true
autorestart=true
redirect_stderr=true
diff --git a/lib/adminpage.js b/lib/adminpage.js
new file mode 100644
index 0000000..4eea1ff
--- /dev/null
+++ b/lib/adminpage.js
@@ -0,0 +1,132 @@
+import ejs from 'ejs';
+import path from 'path';
+import { getConfig, getRedirectBasepath } from '../util/config.js';
+import redisHelper from '../util/redis.js'
+import idp from './idp.js';
+import errorpages from '../util/errorpage.js'
+import { decodeJWT, createJWT } from '../util/jwt.js';
+import log from '../util/logging.js'
+import authz from './authz.js'
+
+var redirectBasePath = getRedirectBasepath()
+
+const route = {
+ from: "ADMIN_INTERNAL",
+ to: "ADMIN_INTERNAL"
+}
+
+async function verifyAdminJwt(req, res, next) {
+ try {
+ var jwt = req.get("X-Veriflow-Admin-Jwt")
+ var decodedJwt = await decodeJWT(jwt)
+ if (!jwt || !decodedJwt) {
+ var html = await errorpages.renderErrorPage(403, null, req)
+ log.access("invalidJwtUsedToAccessAdminPage", route, jwt, req)
+ res.status(403).send(html)
+ return
+ }
+ var allowedGroups = getConfig().admin.allowed_groups
+ var foundGroups = await authz.checkUserGroupMembership(decodedJwt, allowedGroups)
+ if (foundGroups.length > 0) {
+ next()
+ } else {
+ var html = await errorpages.renderErrorPage(403, null, req)
+ log.access("userIsDeniedToAdminPage", route, jwt, req)
+ res.status(403).send(html)
+ return
+ }
+ } catch (error) {
+ var html = await errorpages.renderErrorPage(403, null, req)
+ log.error({ message: "Unknown error occurred in verifyAdminJwt", context: { error: error.message, stack: error.stack } })
+ res.status(403).send(html)
+ return
+ }
+}
+
+async function renderSessionsPage(req, res) {
+ try {
+ var sessions = await redisHelper.getAllSessions()
+ var config = getConfig()
+ var logo_image_src = config?.ui?.logo_image_src || false
+ var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_sessions.ejs'),
+ {
+ sessions,
+ logo_image_src,
+ redirectBasePath
+ });
+ res.send(html)
+ } catch (error) {
+ var html = await errorpages.renderErrorPage(500, null, req)
+ log.error({ message: "Unknown error occurred in renderSessionsPage", context: { error: error.message, stack: error.stack } })
+ res.status(500).send(html)
+ }
+
+}
+
+async function renderUsersPage(req, res) {
+ try {
+ var usersObj = await idp.getAllUsers()
+ console.log(usersObj)
+ var users = []
+ for (var user of Object.keys(usersObj)) {
+ users.push(usersObj[user])
+ }
+ var config = getConfig()
+ var logo_image_src = config?.ui?.logo_image_src || false
+ var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_users.ejs'),
+ {
+ users,
+ logo_image_src,
+ redirectBasePath
+ });
+ res.send(html)
+ } catch (error) {
+ var html = await errorpages.renderErrorPage(500, null, req)
+ log.error({ message: "Unknown error occurred in renderUsersPage", context: { error: error.message, stack: error.stack } })
+ res.status(500).send(html)
+ }
+}
+
+async function renderUserDetailsPage(req, res) {
+ try {
+ var userId = req.query.id
+ if (!userId) {
+ res.redirect(redirectBasePath + "/asmin/users")
+ return
+ }
+ var user = await idp.getUserById(userId)
+ var userGroups = user.groups
+ delete user.groups
+ var config = getConfig()
+ var logo_image_src = config?.ui?.logo_image_src || false
+ var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_user_details.ejs'),
+ {
+ user,
+ userGroups,
+ logo_image_src,
+ redirectBasePath
+ });
+ res.send(html)
+ } catch (error) {
+ var html = await errorpages.renderErrorPage(500, null, req)
+ log.error({ message: "Unknown error occurred in renderUserDetailsPage", context: { error: error.message, stack: error.stack } })
+ res.status(500).send(html)
+ }
+
+}
+
+async function killSession(req, res) {
+ var sessionId = req.query.id
+ if (sessionId) {
+ await redisHelper.deleteSession(sessionId)
+ }
+ res.redirect(redirectBasePath + '/admin/sessions/')
+}
+
+export default {
+ renderSessionsPage,
+ killSession,
+ verifyAdminJwt,
+ renderUsersPage,
+ renderUserDetailsPage
+}
\ No newline at end of file
diff --git a/lib/authz.js b/lib/authz.js
index d2626a1..72eaffb 100644
--- a/lib/authz.js
+++ b/lib/authz.js
@@ -122,5 +122,6 @@ async function getRequestHeaderMapConfig(user, route) {
}
export default {
- authZRequest
+ authZRequest,
+ checkUserGroupMembership
}
\ No newline at end of file
diff --git a/lib/http.js b/lib/http.js
index c264d9f..5dc2cc4 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -11,8 +11,11 @@ import crypto from 'crypto';
import errorpages from '../util/errorpage.js'
import metrics from '../util/metrics.js'
import { randomUUID } from 'node:crypto'
+import admin from './adminpage.js'
-var trusted_ranges = ["loopback"].concat(getConfig().trusted_ranges || [])
+const config = getConfig()
+
+var trusted_ranges = ["loopback"].concat(config.trusted_ranges || [])
log.debug({ message: `Setting trusted proxies to ${trusted_ranges}` })
app.set('trust proxy', trusted_ranges)
@@ -25,10 +28,10 @@ app.use(
store: redisHelper.getRedisStore(),
resave: false,
saveUninitialized: false,
- secret: getConfig().cookie_secret,
+ secret: config.cookie_secret,
cookie: {
...defaultCookieOptions,
- ...getConfig().cookie_settings
+ ...config.cookie_settings
}
})
)
@@ -41,13 +44,13 @@ const getPublicKeyFromPrivateKey = (privateKeyPem) => {
};
function generateJwks() {
- let signing_key = getConfig().signing_key
+ let signing_key = config.signing_key
let buff = Buffer.from(signing_key, 'base64');
let pemPrivateKey = buff.toString('ascii');
const pemPublicKey = getPublicKeyFromPrivateKey(pemPrivateKey);
jwks = pem2jwk(pemPublicKey);
- jwks.kid = getConfig().kid_override || "0"
- jwks.alg = getConfig().signing_key_algorithm || "RS256"
+ jwks.kid = config.kid_override || "0"
+ jwks.alg = config.signing_key_algorithm || "RS256"
jwks.use = "sig"
}
@@ -73,6 +76,16 @@ app.get('/ping', (req, res) => {
res.sendStatus(200)
})
+if (config.enable_admin_panel !== false) {
+ app.use(redirectBasePath + '/admin', admin.verifyAdminJwt)
+ app.get(redirectBasePath + '/admin/sessions', admin.renderSessionsPage)
+ app.get(redirectBasePath + '/admin/sessions/kill', admin.killSession)
+ app.get(redirectBasePath + '/admin/users', admin.renderUsersPage)
+ app.get(redirectBasePath + '/admin/users/details', admin.renderUserDetailsPage)
+}
+
+
+
app.get(redirectBasePath + '/verify', ssoController.verifyAuth)
app.get(redirectBasePath + '/set', ssoController.setSessionCookie)
@@ -80,6 +93,7 @@ app.get(redirectBasePath + '/logout', async (req, res) => {
if (req.session.loggedin) {
await redisHelper.logUserOutAllSessions(req.session.userId)
}
+ req.session.destroy()
var html = await errorpages.renderErrorPage(200, "LOGOUT_SUCCESS", req)
res.send(html)
})
@@ -87,7 +101,7 @@ app.get(redirectBasePath + '/logout', async (req, res) => {
app.get(redirectBasePath + '/auth', ssoController.redirectToSsoProvider)
app.get(redirectBasePath + '/callback', ssoController.verifySsoCallback)
-app.get(getConfig().jwks_path, (req, res) => {
+app.get(config.jwks_path, (req, res) => {
res.json({
keys: [jwks],
});
diff --git a/lib/idp.js b/lib/idp.js
index 40f408b..d47bd67 100644
--- a/lib/idp.js
+++ b/lib/idp.js
@@ -55,6 +55,11 @@ async function getUserById(userId) {
return user
}
+async function getAllUsers() {
+ var users = await adapter.getAllUsers()
+ return users
+}
+
async function addNewUserFromClaims(userClaims) {
if (!adapter.addNewUserFromClaims) {
log.debug({ message: `Adapter ${currentConfig.idp_provider} does not support adding new users via claims, returning`, context: { claims: userClaims } })
@@ -67,5 +72,6 @@ async function addNewUserFromClaims(userClaims) {
export default {
getUserById,
scheduleUpdate,
- addNewUserFromClaims
+ addNewUserFromClaims,
+ getAllUsers
}
\ No newline at end of file
diff --git a/lib/idp_adapters/googleworkspace.js b/lib/idp_adapters/googleworkspace.js
index c8b4550..7485e22 100644
--- a/lib/idp_adapters/googleworkspace.js
+++ b/lib/idp_adapters/googleworkspace.js
@@ -99,7 +99,13 @@ async function getUserById(id) {
return config[id]
}
+async function getAllUsers() {
+ var config = await getIdpConfig()
+ return config
+}
+
export default {
runUpdate,
- getUserById
+ getUserById,
+ getAllUsers
};
diff --git a/lib/idp_adapters/localfile.js b/lib/idp_adapters/localfile.js
index ce167cb..5767b7f 100644
--- a/lib/idp_adapters/localfile.js
+++ b/lib/idp_adapters/localfile.js
@@ -24,7 +24,13 @@ async function getUserById(id) {
return config[id]
}
+async function getAllUsers() {
+ var config = await getLocalIdpConfig()
+ return config
+}
+
export default {
runUpdate,
- getUserById
+ getUserById,
+ getAllUsers
};
\ No newline at end of file
diff --git a/lib/idp_adapters/msgraph.js b/lib/idp_adapters/msgraph.js
index 6525fb7..66aacc0 100644
--- a/lib/idp_adapters/msgraph.js
+++ b/lib/idp_adapters/msgraph.js
@@ -120,8 +120,13 @@ async function getUserById(id) {
return config[id]
}
+async function getAllUsers() {
+ var config = await getIdpConfig()
+ return config
+}
export default {
runUpdate,
- getUserById
+ getUserById,
+ getAllUsers
};
\ No newline at end of file
diff --git a/lib/idp_adapters/tokenclaims.js b/lib/idp_adapters/tokenclaims.js
index ca879f6..424d6ed 100644
--- a/lib/idp_adapters/tokenclaims.js
+++ b/lib/idp_adapters/tokenclaims.js
@@ -43,8 +43,37 @@ async function addNewUserFromClaims(claims) {
await redisClient.expire(`veriflow:users:${userId}`, 87000); // expire in 24 hours
}
+async function getAllUsers() {
+ const keys = await scanKeys('veriflow:users:*');
+ const users = [];
+ for (const key of keys) {
+ const userDataRaw = await redisClient.get(key);
+ if (userDataRaw) {
+ let userData = JSON.parse(userDataRaw);
+ // Extract session ID from key and add it to the session object
+ const userId = key.replace('veriflow:users:', '');
+ userData.userId = userId;
+ users.push(userData);
+ }
+ }
+ return users;
+}
+
+// Utility function to use SCAN for fetching keys without blocking the server
+async function scanKeys(pattern) {
+ let cursor = '0';
+ const keys = [];
+ do {
+ const reply = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', '100');
+ cursor = reply[0];
+ keys.push(...reply[1]);
+ } while (cursor !== '0');
+ return keys;
+}
+
export default {
runUpdate,
getUserById,
- addNewUserFromClaims
+ addNewUserFromClaims,
+ getAllUsers
};
\ No newline at end of file
diff --git a/lib/sso.js b/lib/sso.js
index 3fac26c..66afe66 100644
--- a/lib/sso.js
+++ b/lib/sso.js
@@ -126,8 +126,6 @@ async function verifyAuth(req, res) {
*/
async function setSessionCookie(req, res) {
try {
- // First get the current config
- var currentConfig = getConfig()
// If the request contains a token param, verify it, and log the user in by setting a session variable.
// Finally, redirect the user to the originally requested URL, which was also specified in the JWT
if (req.query.token) {
@@ -141,7 +139,8 @@ async function setSessionCookie(req, res) {
req.session.loggedin = true
req.session.userId = decoded.userId;
-
+ await addSessionDetails(req)
+ req.session.details.parent_session_id = decoded.parentSession
// This sets the cookie for the accessed domain to expire at the same time as the "main" veriflow cookie, to
// prevent a user from being deauthenticated from Veriflow, but still authenticated on the subdomains.
var expireDate = false
@@ -210,14 +209,15 @@ async function redirectToSsoProvider(req, res) {
var redirectBasePath = getRedirectBasepath()
if (req.session.loggedin) {
- req.session.touch()
+ await addSessionDetails(req)
let jwtPayload = {
protocol: redirectToken.protocol,
host: redirectToken.host,
path: redirectToken.path,
query: redirectToken.query,
userId: req.session.userId,
- cookieExpires: req.session.cookie.expires
+ cookieExpires: req.session.cookie.expires,
+ parentSession: req.sessionID
}
var signedJwt = await createJWT(jwtPayload)
@@ -296,6 +296,7 @@ async function verifySsoCallback(req, res) {
req.session.loggedin = true;
req.session.userId = userIdClaim;
+ await addSessionDetails(req)
let jwtPayload = {
protocol: req.session.redirect.protocol,
@@ -303,7 +304,8 @@ async function verifySsoCallback(req, res) {
path: req.session.redirect.path,
query: req.session.redirect.query,
userId: userIdClaim,
- cookieExpires: req.session.cookie.expires
+ cookieExpires: req.session.cookie.expires,
+ parentSession: req.sessionID
}
var signedJwt = await createJWT(jwtPayload)
@@ -325,6 +327,14 @@ async function verifySsoCallback(req, res) {
}
}
+async function addSessionDetails(req) {
+ req.session.details = {
+ user_agent: req.get("user-agent"),
+ remote_ip: req.ip,
+ original_host: req.get('x-forwarded-host')
+ }
+}
+
export default {
verifySsoCallback,
redirectToSsoProvider,
diff --git a/package.json b/package.json
index b805f97..cdeb651 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "app.js",
"type": "module",
"scripts": {
- "start": "npx nodemon app.js"
+ "start": "npx nodemon app.js -e ejs,js,css,html,jpg,png,scss"
},
"repository": {
"type": "git",
diff --git a/test/e2e/configs/dex.yaml b/test/e2e/configs/dex.yaml
index 887a9b2..8b5009e 100644
--- a/test/e2e/configs/dex.yaml
+++ b/test/e2e/configs/dex.yaml
@@ -29,4 +29,9 @@ staticPasswords:
# bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "test"
- userID: "test@veriflow.dev"
\ No newline at end of file
+ userID: "test@veriflow.dev"
+- email: "denied@veriflow.dev"
+ # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
+ username: "denied"
+ userID: "denied@veriflow.dev"
\ No newline at end of file
diff --git a/test/e2e/configs/idp_output.json b/test/e2e/configs/idp_output.json
index 0ba37b6..a85e3e4 100644
--- a/test/e2e/configs/idp_output.json
+++ b/test/e2e/configs/idp_output.json
@@ -10,5 +10,16 @@
"All Users",
"test-header-group"
]
+ },
+ "denied@veriflow.dev": {
+ "displayName": "Veriflow Denied User",
+ "givenName": "Denied",
+ "preferredLanguage": "en",
+ "surname": "User",
+ "userPrincipalName": "denied@veriflow.dev",
+ "id": "denied@veriflow.dev",
+ "groups": [
+ "No Users"
+ ]
}
}
\ No newline at end of file
diff --git a/test/e2e/configs/veriflow.yaml b/test/e2e/configs/veriflow.yaml
index fdbe2e0..060fb52 100644
--- a/test/e2e/configs/veriflow.yaml
+++ b/test/e2e/configs/veriflow.yaml
@@ -1,4 +1,8 @@
---
+admin:
+ enable: true
+ allowed_groups:
+ - All Users
data_listen_port: 80
metrics_listen_port: 9090
service_url: http://veriflow.localtest.me
diff --git a/test/e2e/tests/admin_page_test.js b/test/e2e/tests/admin_page_test.js
new file mode 100644
index 0000000..646e6a1
--- /dev/null
+++ b/test/e2e/tests/admin_page_test.js
@@ -0,0 +1,17 @@
+Feature('Admin Page').retry(3);
+
+Scenario('I can login to admin page', async ({ I }) => {
+ I.amOnPage('http://veriflow.localtest.me/.veriflow/admin/sessions');
+ I.login();
+ I.see("Sessions")
+});
+
+Scenario('I cant login to admin page as non admin', async ({ I }) => {
+ I.amOnPage('http://veriflow.localtest.me/.veriflow/admin/sessions');
+ I.fillField('login', 'denied@veriflow.dev');
+ I.fillField('password', 'password');
+ I.click('Login');
+ I.see("Unauthorized")
+});
+
+
diff --git a/util/caddyModels.js b/util/caddyModels.js
index 9153609..b84aaad 100644
--- a/util/caddyModels.js
+++ b/util/caddyModels.js
@@ -1,4 +1,4 @@
-import { getConfig, getAuthListenPort } from "./config.js"
+import { getConfig, getAuthListenPort, getRedirectBasepath } from "./config.js"
import log from './logging.js';
import { writeFile, stat } from "fs/promises";
import axios from 'axios';
@@ -304,6 +304,26 @@ async function saturateAllRoutesFromConfig(config) {
async function generateCaddyConfig() {
log.debug("Generating new caddy config")
var config = getConfig()
+
+ if (config?.admin?.enable !== false && config?.admin?.allowed_groups) {
+ log.info("Admin panel will be enabled")
+ var serviceUrl = config.service_url
+ var baseRedirectUrl = getRedirectBasepath()
+ var adminUrl = new URL(`${serviceUrl}${baseRedirectUrl}/admin`)
+ var adminPanelRoute = {
+ from: {
+ host: [adminUrl.hostname],
+ path: [getRedirectBasepath() + "/admin/*"]
+ },
+ to: "http://localhost:" + getAuthListenPort(),
+ allowed_groups: config.admin.allowed_groups,
+ claims_headers: {
+ "X-Veriflow-Admin-Jwt": "jwt"
+ }
+ }
+ config.policy.unshift(adminPanelRoute)
+ }
+
var routes = await saturateAllRoutesFromConfig(config)
var requestIdRoute = {
@@ -367,6 +387,14 @@ async function generateCaddyConfig() {
{
"host": [
serviceUrl.hostname
+ ],
+ "path": [
+ "/ping",
+ getRedirectBasepath() + "/verify",
+ getRedirectBasepath() + "/set",
+ getRedirectBasepath() + "/logout",
+ getRedirectBasepath() + "/auth",
+ getRedirectBasepath() + "/callback"
]
}
],
diff --git a/util/redis.js b/util/redis.js
index d6d0aab..0e110b4 100644
--- a/util/redis.js
+++ b/util/redis.js
@@ -77,9 +77,15 @@ async function scanKeys(pattern) {
return keys;
}
+async function deleteSession(sessionId) {
+ await redis.del(`vfsession:${sessionId}`);
+}
+
export default {
getClient,
getRedisConfig,
getRedisStore,
- logUserOutAllSessions
+ logUserOutAllSessions,
+ getAllSessions,
+ deleteSession
}
\ No newline at end of file
diff --git a/util/utils.js b/util/utils.js
index a80669f..0000e66 100644
--- a/util/utils.js
+++ b/util/utils.js
@@ -18,6 +18,11 @@ function convertHeaderCase(str) {
.join('-'); // Rejoin the words with hyphens
}
+async function addSessionMetadataToRequest(req) {
+ var userAgent
+ var requestingIp
+}
+
export default {
urlToCaddyUpstream,
convertHeaderCase
diff --git a/views/admin_sessions.ejs b/views/admin_sessions.ejs
new file mode 100644
index 0000000..5069ef9
--- /dev/null
+++ b/views/admin_sessions.ejs
@@ -0,0 +1,60 @@
+
+
+
+ <%- include('includes/admin_head'); %>
+
+
+
+
+
+
+
+
Current Sessions
+
+
+
+
+ Session ID |
+ Parent Session ID |
+ Username |
+ Accessed Host |
+ Session Expires |
+ Connecting IP |
+ User Agent |
+ Actions |
+
+
+
+ <% for (const session of sessions) { %>
+
+ <%= session.sessionId %> |
+ <%= session?.details?.parent_session_id %> |
+ <%= session.userId %> |
+ <%= session?.details?.original_host %> |
+ <%= session.cookie.expires %> |
+ <%= session?.details?.remote_ip %> |
+ <%= session?.details?.user_agent %> |
+ |
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/admin_user_details.ejs b/views/admin_user_details.ejs
new file mode 100644
index 0000000..e32b3b8
--- /dev/null
+++ b/views/admin_user_details.ejs
@@ -0,0 +1,69 @@
+
+
+
+ <%- include('includes/admin_head'); %>
+
+
+
+
+
+
+
+
User Details - <%= user.id %>
+
+
+
+
+ Key |
+ Value |
+
+
+
+ <% for (const d of Object.keys(user)) { %>
+
+ <%= d %> |
+ <%= user[d] %> |
+
+ <% } %>
+
+
+
+
+
+
+
User Groups
+
+
+
+
+ Group Name |
+
+
+
+ <% for (const d of userGroups) { %>
+
+ <%= d %> |
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/admin_users.ejs b/views/admin_users.ejs
new file mode 100644
index 0000000..70d9315
--- /dev/null
+++ b/views/admin_users.ejs
@@ -0,0 +1,48 @@
+
+
+
+ <%- include('includes/admin_head'); %>
+
+
+
+
+
+
+
+
Current Users
+
+
+
+
+ User ID |
+ Number Groups |
+
+
+
+ <% for (const user of users) { %>
+
+ <%= user.id %> |
+ <%= user.groups.length %> |
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/error_fullpage.ejs b/views/error_fullpage.ejs
index 236bf0b..86e833e 100644
--- a/views/error_fullpage.ejs
+++ b/views/error_fullpage.ejs
@@ -2,7 +2,7 @@
- <%- include('includes/head'); %>
+ <%- include('includes/error_head'); %>
diff --git a/views/includes/admin_head.ejs b/views/includes/admin_head.ejs
new file mode 100644
index 0000000..843a459
--- /dev/null
+++ b/views/includes/admin_head.ejs
@@ -0,0 +1,6 @@
+
+
+Veriflow Admin Dashboard
+
+<%- include('normalize'); %>
+<%- include('admin_style'); %>
\ No newline at end of file
diff --git a/views/includes/admin_style.ejs b/views/includes/admin_style.ejs
new file mode 100644
index 0000000..3c8504a
--- /dev/null
+++ b/views/includes/admin_style.ejs
@@ -0,0 +1,75 @@
+
\ No newline at end of file
diff --git a/views/includes/head.ejs b/views/includes/error_head.ejs
similarity index 66%
rename from views/includes/head.ejs
rename to views/includes/error_head.ejs
index d153190..d6f5a6d 100644
--- a/views/includes/head.ejs
+++ b/views/includes/error_head.ejs
@@ -1,7 +1,6 @@
-
<%= title %>
<%- include('normalize'); %>
-<%- include('style'); %>
\ No newline at end of file
+<%- include('error_style'); %>
\ No newline at end of file
diff --git a/views/includes/style.ejs b/views/includes/error_style.ejs
similarity index 100%
rename from views/includes/style.ejs
rename to views/includes/error_style.ejs