Skip to content

Commit 5fa0897

Browse files
authored
Merge pull request #3665 from mikiher/subdirectory-fixes-3
Subdirectory support for OIDC and SocketIO
2 parents 95c80a5 + 33aa4f1 commit 5fa0897

File tree

10 files changed

+345
-88
lines changed

10 files changed

+345
-88
lines changed

client/pages/config/authentication.vue

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@
6464
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
6565
<p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
6666

67+
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
68+
<div class="w-44">
69+
<ui-dropdown v-model="newAuthSettings.authOpenIDSubfolderForRedirectURLs" small :items="subfolderOptions" :label="$strings.LabelWebRedirectURLsSubfolder" :disabled="savingSettings" />
70+
</div>
71+
<div class="mt-2 sm:mt-5">
72+
<p class="sm:pl-4 text-sm text-gray-300">{{ $strings.LabelWebRedirectURLsDescription }}</p>
73+
<p class="sm:pl-4 text-sm text-gray-300 mb-2">
74+
<code>{{ webCallbackURL }}</code>
75+
<br />
76+
<code>{{ mobileAppCallbackURL }}</code>
77+
</p>
78+
</div>
79+
</div>
80+
6781
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
6882

6983
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
@@ -164,6 +178,27 @@ export default {
164178
value: 'username'
165179
}
166180
]
181+
},
182+
subfolderOptions() {
183+
const options = [
184+
{
185+
text: 'None',
186+
value: ''
187+
}
188+
]
189+
if (this.$config.routerBasePath) {
190+
options.push({
191+
text: this.$config.routerBasePath,
192+
value: this.$config.routerBasePath
193+
})
194+
}
195+
return options
196+
},
197+
webCallbackURL() {
198+
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/callback`
199+
},
200+
mobileAppCallbackURL() {
201+
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/mobile-redirect`
167202
}
168203
},
169204
methods: {
@@ -325,7 +360,8 @@ export default {
325360
},
326361
init() {
327362
this.newAuthSettings = {
328-
...this.authSettings
363+
...this.authSettings,
364+
authOpenIDSubfolderForRedirectURLs: this.authSettings.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : this.authSettings.authOpenIDSubfolderForRedirectURLs
329365
}
330366
this.enableLocalAuth = this.authMethods.includes('local')
331367
this.enableOpenIDAuth = this.authMethods.includes('openid')

client/strings/en-us.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@
679679
"LabelViewPlayerSettings": "View player settings",
680680
"LabelViewQueue": "View player queue",
681681
"LabelVolume": "Volume",
682+
"LabelWebRedirectURLsDescription": "Authorize these URLs in your OAuth provider to allow redirection back to the web app after login:",
683+
"LabelWebRedirectURLsSubfolder": "Subfolder for Redirect URLs",
682684
"LabelWeekdaysToRun": "Weekdays to run",
683685
"LabelXBooks": "{0} books",
684686
"LabelXItems": "{0} items",

server/Auth.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class Auth {
131131
{
132132
client: openIdClient,
133133
params: {
134-
redirect_uri: '/auth/openid/callback',
134+
redirect_uri: `${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/callback`,
135135
scope: 'openid profile email'
136136
}
137137
},
@@ -480,9 +480,9 @@ class Auth {
480480
// for the request to mobile-redirect and as such the session is not shared
481481
this.openIdAuthSession.set(state, { mobile_redirect_uri: req.query.redirect_uri })
482482

483-
redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString()
483+
redirectUri = new URL(`${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/mobile-redirect`, hostUrl).toString()
484484
} else {
485-
redirectUri = new URL('/auth/openid/callback', hostUrl).toString()
485+
redirectUri = new URL(`${global.ServerSettings.authOpenIDSubfolderForRedirectURLs}/auth/openid/callback`, hostUrl).toString()
486486

487487
if (req.query.state) {
488488
Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`)
@@ -733,7 +733,7 @@ class Auth {
733733
const host = req.get('host')
734734
// TODO: ABS does currently not support subfolders for installation
735735
// If we want to support it we need to include a config for the serverurl
736-
postLogoutRedirectUri = `${protocol}://${host}/login`
736+
postLogoutRedirectUri = `${protocol}://${host}${global.RouterBasePath}/login`
737737
}
738738
// else for openid-mobile we keep postLogoutRedirectUri on null
739739
// nice would be to redirect to the app here, but for example Authentik does not implement

server/Server.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ class Server {
8484
Logger.logManager = new LogManager()
8585

8686
this.server = null
87-
this.io = null
8887
}
8988

9089
/**
@@ -441,18 +440,11 @@ class Server {
441440
async stop() {
442441
Logger.info('=== Stopping Server ===')
443442
Watcher.close()
444-
Logger.info('Watcher Closed')
445-
446-
return new Promise((resolve) => {
447-
SocketAuthority.close((err) => {
448-
if (err) {
449-
Logger.error('Failed to close server', err)
450-
} else {
451-
Logger.info('Server successfully closed')
452-
}
453-
resolve()
454-
})
455-
})
443+
Logger.info('[Server] Watcher Closed')
444+
await SocketAuthority.close()
445+
Logger.info('[Server] Closing HTTP Server')
446+
await new Promise((resolve) => this.server.close(resolve))
447+
Logger.info('[Server] HTTP Server Closed')
456448
}
457449
}
458450
module.exports = Server

server/SocketAuthority.js

Lines changed: 82 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Auth = require('./Auth')
1414
class SocketAuthority {
1515
constructor() {
1616
this.Server = null
17-
this.io = null
17+
this.socketIoServers = []
1818

1919
/** @type {Object.<string, SocketClient>} */
2020
this.clients = {}
@@ -89,82 +89,104 @@ class SocketAuthority {
8989
*
9090
* @param {Function} callback
9191
*/
92-
close(callback) {
93-
Logger.info('[SocketAuthority] Shutting down')
94-
// This will close all open socket connections, and also close the underlying http server
95-
if (this.io) this.io.close(callback)
96-
else callback()
92+
async close() {
93+
Logger.info('[SocketAuthority] closing...')
94+
const closePromises = this.socketIoServers.map((io) => {
95+
return new Promise((resolve) => {
96+
Logger.info(`[SocketAuthority] Closing Socket.IO server: ${io.path}`)
97+
io.close(() => {
98+
Logger.info(`[SocketAuthority] Socket.IO server closed: ${io.path}`)
99+
resolve()
100+
})
101+
})
102+
})
103+
await Promise.all(closePromises)
104+
Logger.info('[SocketAuthority] closed')
105+
this.socketIoServers = []
97106
}
98107

99108
initialize(Server) {
100109
this.Server = Server
101110

102-
this.io = new SocketIO.Server(this.Server.server, {
111+
const socketIoOptions = {
103112
cors: {
104113
origin: '*',
105114
methods: ['GET', 'POST']
106-
},
107-
path: `${global.RouterBasePath}/socket.io`
108-
})
109-
110-
this.io.on('connection', (socket) => {
111-
this.clients[socket.id] = {
112-
id: socket.id,
113-
socket,
114-
connected_at: Date.now()
115115
}
116-
socket.sheepClient = this.clients[socket.id]
116+
}
117117

118-
Logger.info('[SocketAuthority] Socket Connected', socket.id)
118+
const ioServer = new SocketIO.Server(Server.server, socketIoOptions)
119+
ioServer.path = '/socket.io'
120+
this.socketIoServers.push(ioServer)
119121

120-
// Required for associating a User with a socket
121-
socket.on('auth', (token) => this.authenticateSocket(socket, token))
122+
if (global.RouterBasePath) {
123+
// open a separate socket.io server for the router base path, keeping the original server open for legacy clients
124+
const ioBasePath = `${global.RouterBasePath}/socket.io`
125+
const ioBasePathServer = new SocketIO.Server(Server.server, { ...socketIoOptions, path: ioBasePath })
126+
ioBasePathServer.path = ioBasePath
127+
this.socketIoServers.push(ioBasePathServer)
128+
}
122129

123-
// Scanning
124-
socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId))
130+
this.socketIoServers.forEach((io) => {
131+
io.on('connection', (socket) => {
132+
this.clients[socket.id] = {
133+
id: socket.id,
134+
socket,
135+
connected_at: Date.now()
136+
}
137+
socket.sheepClient = this.clients[socket.id]
125138

126-
// Logs
127-
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
128-
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
139+
Logger.info(`[SocketAuthority] Socket Connected to ${io.path}`, socket.id)
129140

130-
// Sent automatically from socket.io clients
131-
socket.on('disconnect', (reason) => {
132-
Logger.removeSocketListener(socket.id)
141+
// Required for associating a User with a socket
142+
socket.on('auth', (token) => this.authenticateSocket(socket, token))
133143

134-
const _client = this.clients[socket.id]
135-
if (!_client) {
136-
Logger.warn(`[SocketAuthority] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
137-
} else if (!_client.user) {
138-
Logger.info(`[SocketAuthority] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
139-
delete this.clients[socket.id]
140-
} else {
141-
Logger.debug('[SocketAuthority] User Offline ' + _client.user.username)
142-
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
144+
// Scanning
145+
socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId))
143146

144-
const disconnectTime = Date.now() - _client.connected_at
145-
Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
146-
delete this.clients[socket.id]
147-
}
148-
})
147+
// Logs
148+
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
149+
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
149150

150-
//
151-
// Events for testing
152-
//
153-
socket.on('message_all_users', (payload) => {
154-
// admin user can send a message to all authenticated users
155-
// displays on the web app as a toast
156-
const client = this.clients[socket.id] || {}
157-
if (client.user?.isAdminOrUp) {
158-
this.emitter('admin_message', payload.message || '')
159-
} else {
160-
Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`)
161-
}
162-
})
163-
socket.on('ping', () => {
164-
const client = this.clients[socket.id] || {}
165-
const user = client.user || {}
166-
Logger.debug(`[SocketAuthority] Received ping from socket ${user.username || 'No User'}`)
167-
socket.emit('pong')
151+
// Sent automatically from socket.io clients
152+
socket.on('disconnect', (reason) => {
153+
Logger.removeSocketListener(socket.id)
154+
155+
const _client = this.clients[socket.id]
156+
if (!_client) {
157+
Logger.warn(`[SocketAuthority] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
158+
} else if (!_client.user) {
159+
Logger.info(`[SocketAuthority] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
160+
delete this.clients[socket.id]
161+
} else {
162+
Logger.debug('[SocketAuthority] User Offline ' + _client.user.username)
163+
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
164+
165+
const disconnectTime = Date.now() - _client.connected_at
166+
Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
167+
delete this.clients[socket.id]
168+
}
169+
})
170+
171+
//
172+
// Events for testing
173+
//
174+
socket.on('message_all_users', (payload) => {
175+
// admin user can send a message to all authenticated users
176+
// displays on the web app as a toast
177+
const client = this.clients[socket.id] || {}
178+
if (client.user?.isAdminOrUp) {
179+
this.emitter('admin_message', payload.message || '')
180+
} else {
181+
Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`)
182+
}
183+
})
184+
socket.on('ping', () => {
185+
const client = this.clients[socket.id] || {}
186+
const user = client.user || {}
187+
Logger.debug(`[SocketAuthority] Received ping from socket ${user.username || 'No User'}`)
188+
socket.emit('pong')
189+
})
168190
})
169191
})
170192
}

server/controllers/MiscController.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,9 +679,9 @@ class MiscController {
679679
continue
680680
}
681681
let updatedValue = settingsUpdate[key]
682-
if (updatedValue === '') updatedValue = null
682+
if (updatedValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') updatedValue = null
683683
let currentValue = currentAuthenticationSettings[key]
684-
if (currentValue === '') currentValue = null
684+
if (currentValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') currentValue = null
685685

686686
if (updatedValue !== currentValue) {
687687
Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`)

server/migrations/changelog.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time.
44

5-
| Server Version | Migration Script Name | Description |
6-
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
7-
| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
8-
| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
9-
| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes |
10-
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
11-
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
5+
| Server Version | Migration Script Name | Description |
6+
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
7+
| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
8+
| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
9+
| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes |
10+
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
11+
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
12+
| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations |

0 commit comments

Comments
 (0)