Added Login with OIDC button to login page. Made login.ejs dynamic based on enabled authentication services. Made GitHub icon on login.ejs smaller. Refactored authorization.js middleware to support running both internal and OIDC authentication within the same instance. Added extra error to info.js when both authentication services are disabled but authentication itself is enabled. Updated status.js to correctly display both authentication services running at the same time. Updated README.md. Enabled /login when OIDC is enabled. Added missing middleware on /logout. Fixed JWT not initializing when authInternalEnabled is true

This commit is contained in:
Glenn de Haan
2024-10-03 13:56:28 +02:00
parent e1c1aa8c21
commit 4418f9c347
6 changed files with 123 additions and 67 deletions

View File

@@ -311,8 +311,6 @@ To enable OIDC authentication, set the following environment variables in your a
- **`AUTH_OIDC_CLIENT_SECRET`**:
The client secret associated with your OIDC provider. This value is specific to the OIDC client created for the UniFi Voucher Site.
> Please note that **enabling OIDC support will automatically disable the built-in login system**. Once OIDC is activated, all user authentication will be handled through your configured identity provider, and the local login mechanism will no longer be available.
> Ensure your idP supports **Confidential Clients** with the **Authorization Code Flow**
#### Determine Supported Client Types
@@ -363,6 +361,8 @@ If you prefer not to use any authentication for the web and api service, you can
AUTH_DISABLE: 'true'
```
> Note: This disables the token based authentication on the API
## Print Functionality
The UniFi Voucher Site application includes built-in support for printing vouchers using 80mm receipt printers, offering a convenient way to distribute vouchers in physical format.

View File

@@ -24,34 +24,48 @@ module.exports = {
* @return {Promise<void>}
*/
web: async (req, res, next) => {
// Check if authentication is enabled & OIDC is disabled
if(!variables.authDisabled && !variables.authOidcEnabled) {
// Check if user has an existing authorization cookie
if (!req.cookies.authorization) {
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
let internal = false;
let oidc = false;
// Continue is authentication is disabled
if(variables.authDisabled) {
next();
return;
}
// Check if Internal auth is enabled then verify user status
if(variables.authInternalEnabled) {
// Check if user has an existing authorization cookie
if (req.cookies.authorization) {
// Check if token is correct and valid
try {
const check = jwt.verify(req.cookies.authorization);
if(!check) {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid or expired login!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
if(check) {
internal = true;
}
} catch (e) {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid or expired login!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
return;
} catch (e) {}
}
}
// Check if authentication is enabled & OIDC is enabled
if(!variables.authDisabled && variables.authOidcEnabled) {
const middleware = oidc.requiresAuth();
return middleware(req, res, next);
// Check if OIDC is enabled then verify user status
if(variables.authOidcEnabled) {
oidc = req.oidc.isAuthenticated();
}
// Check if user is authorized by a service
if(internal || oidc) {
// Remove req.oidc if user is authenticated internally
if(internal) {
delete req.oidc;
}
next();
return;
}
// Fallback to login page
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
},
/**

View File

@@ -72,7 +72,14 @@ module.exports = () => {
/**
* Log auth status
*/
log.info(`[Auth] ${variables.authDisabled ? 'Disabled!' : `Enabled! Type: ${variables.authOidcEnabled ? 'OIDC' : 'Internal'}`}`);
log.info(`[Auth] ${variables.authDisabled ? 'Disabled!' : `Enabled! Type: ${variables.authInternalEnabled ? 'Internal' : ''}${variables.authInternalEnabled && variables.authOidcEnabled ? ', ' : ''}${variables.authOidcEnabled ? 'OIDC' : ''}`}`);
/**
* Check auth services
*/
if(!variables.authDisabled && !variables.authInternalEnabled && !variables.authOidcEnabled) {
log.error(`[Auth] Incorrect Configuration Detected!. Authentication is enabled but all authentication services have been disabled`);
}
/**
* Verify OIDC configuration

View File

@@ -48,7 +48,7 @@ info();
/**
* Initialize JWT
*/
if(!variables.authDisabled && !variables.authOidcEnabled) {
if(!variables.authDisabled && variables.authInternalEnabled) {
jwt.init();
}
@@ -124,7 +124,6 @@ app.get('/', (req, res) => {
// Check if web service is enabled
if(variables.serviceWeb) {
if(!variables.authOidcEnabled) {
app.get('/login', (req, res) => {
// Check if authentication is disabled
if (variables.authDisabled) {
@@ -139,10 +138,18 @@ if(variables.serviceWeb) {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
error: req.flashMessage.type === 'error',
error_text: req.flashMessage.message || '',
app_header: timeHeader
app_header: timeHeader,
internalAuth: variables.authInternalEnabled,
oidcAuth: variables.authOidcEnabled
});
});
app.post('/login', async (req, res) => {
// Check if internal authentication is enabled
if(!variables.authInternalEnabled) {
res.status(501).send();
return;
}
if (typeof req.body === "undefined") {
res.status(400).send();
return;
@@ -157,8 +164,7 @@ if(variables.serviceWeb) {
res.cookie('authorization', jwt.sign(), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
});
}
app.get('/logout', (req, res) => {
app.get('/logout', [authorization.web], (req, res) => {
// Check if authentication is disabled
if (variables.authDisabled) {
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);

View File

@@ -50,6 +50,7 @@
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<% if(internalAuth) { %>
<form class="space-y-6" action="<%= baseUrl %>/login" method="post" enctype="multipart/form-data">
<div>
<label for="password" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Password</label>
@@ -62,10 +63,38 @@
<button type="submit" class="flex w-full justify-center rounded-md bg-sky-700 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Sign in</button>
</div>
</form>
<% } %>
<% if(oidcAuth) { %>
<div>
<% if(internalAuth) { %>
<div class="relative mt-10">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-black/5 dark:border-white/5"></div>
</div>
<div class="relative flex justify-center text-sm font-medium leading-6">
<span class="bg-gray-100 dark:bg-gray-900 px-6 text-gray-900 dark:text-white">Or continue with</span>
</div>
</div>
<% } %>
<div class="<%= internalAuth ? 'mt-6' : '' %>">
<a href="<%= baseUrl %>/oidc/login" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent">
<svg class="h-5 w-5" viewBox="0 0 64 64" aria-hidden="true">
<path fill="#f7931e" d="M29.1 6.4v54.5l9.7-4.5V1.7l-9.7 4.7z"></path>
<path fill="#b2b2b2" d="M62.7 22.4L64 36.3l-18.7-4.1"></path>
<path d="M40.5 19.6v6.2a29.54 29.54 0 0 1 10.6 3.8l6.9-4.2a43.585 43.585 0 0 0-17.5-5.8M9.7 40.2c0-6.9 7.5-12.7 17.7-14.4v-6.2C11.8 21.5 0 30 0 40.2 0 50.8 12.6 59.5 29.1 61v-6.1C18 53.5 9.7 47.4 9.7 40.2" fill="#b2b2b2"></path>
<path d="M40.5 19.6v6.2a29.54 29.54 0 0 1 10.6 3.8l6.9-4.2a43.585 43.585 0 0 0-17.5-5.8M9.7 40.2c0-6.9 7.5-12.7 17.7-14.4v-6.2C11.8 21.5 0 30 0 40.2 0 50.8 12.6 59.5 29.1 61v-6.1C18 53.5 9.7 47.4 9.7 40.2" fill="#b2b2b2"></path>
</svg>
<span class="text-sm font-semibold leading-6">OpenID Connect</span>
</a>
</div>
</div>
<% } %>
<p class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">
<a href="https://github.com/glenndehaan/unifi-voucher-site" aria-label="GitHub Project Link" target="_blank" rel="noreferrer noopener" class="hover:text-gray-600 dark:hover:text-gray-500">
<svg class="inline-block w-10" viewBox="0 0 42 42" fill="currentColor">
<svg class="inline-block w-8" viewBox="0 0 42 42" fill="currentColor">
<path d="M21,0.5c-11.6,0-21,9.4-21,21c0,9.3,6,17.1,14.4,19.9c1.1,0.2,1.4-0.5,1.4-1c0-0.5,0-1.8,0-3.6C9.9,38.1,8.7,34,8.7,34c-1-2.4-2.3-3.1-2.3-3.1c-1.9-1.3,0.1-1.3,0.1-1.3c2.1,0.1,3.2,2.2,3.2,2.2c1.9,3.2,4.9,2.3,6.1,1.7c0.2-1.4,0.7-2.3,1.3-2.8c-4.7-0.5-9.6-2.3-9.6-10.4c0-2.3,0.8-4.2,2.2-5.6c-0.2-0.5-0.9-2.7,0.2-5.6c0,0,1.8-0.6,5.8,2.2c1.7-0.5,3.5-0.7,5.3-0.7c1.8,0,3.6,0.2,5.3,0.7c4-2.7,5.8-2.2,5.8-2.2c1.1,2.9,0.4,5,0.2,5.6c1.3,1.5,2.2,3.3,2.2,5.6c0,8.1-4.9,9.8-9.6,10.4c0.8,0.6,1.4,1.9,1.4,3.9c0,2.8,0,5.1,0,5.8c0,0.6,0.4,1.2,1.4,1C36,38.7,42,30.8,42,21.5C42,9.9,32.6,0.5,21,0.5z"></path>
</svg>
</a>

View File

@@ -90,10 +90,10 @@ module.exports = () => {
modules: {
internal: {
status: {
text: (!variables.authDisabled && !variables.authOidcEnabled) ? 'Enabled' : 'Disabled',
state: (!variables.authDisabled && !variables.authOidcEnabled) ? 'green' : 'red'
text: (!variables.authDisabled && variables.authInternalEnabled) ? 'Enabled' : 'Disabled',
state: (!variables.authDisabled && variables.authInternalEnabled) ? 'green' : 'red'
},
details: (!variables.authDisabled && !variables.authOidcEnabled) ? 'Internal Authentication enabled.' : 'Internal Authentication not enabled.',
details: (!variables.authDisabled && variables.authInternalEnabled) ? 'Internal Authentication enabled.' : 'Internal Authentication not enabled.',
info: 'https://github.com/glenndehaan/unifi-voucher-site#1-internal-authentication-default'
},
oidc: {