diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue
index 4fa8e394f1..2009b28ddb 100644
--- a/client/components/ui/MultiSelect.vue
+++ b/client/components/ui/MultiSelect.vue
@@ -50,7 +50,11 @@ export default {
label: String,
disabled: Boolean,
readonly: Boolean,
- showEdit: Boolean
+ showEdit: Boolean,
+ menuDisabled: {
+ type: Boolean,
+ default: false
+ },
},
data() {
return {
@@ -77,7 +81,7 @@ export default {
}
},
showMenu() {
- return this.isFocused
+ return this.isFocused && !this.menuDisabled
},
wrapperClass() {
var classes = []
diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue
index e645569e61..9e02830714 100644
--- a/client/pages/config/authentication.vue
+++ b/client/pages/config/authentication.vue
@@ -46,6 +46,9 @@
audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Více",
"LabelMoreInfo": "Více informací",
"LabelName": "Jméno",
diff --git a/client/strings/da.json b/client/strings/da.json
index a93507c08b..fa28dd2449 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minut",
"LabelMissing": "Mangler",
"LabelMissingParts": "Manglende dele",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Mere",
"LabelMoreInfo": "Mere info",
"LabelName": "Navn",
diff --git a/client/strings/de.json b/client/strings/de.json
index 3487bdec13..da8af1d8a0 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minute",
"LabelMissing": "Fehlend",
"LabelMissingParts": "Fehlende Teile",
+ "LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
+ "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist audiobookshelf://oauth
, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (*
) als alleiniger Eintrag erlaubt jede URI.",
"LabelMore": "Mehr",
"LabelMoreInfo": "Mehr Info",
"LabelName": "Name",
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 857627e950..02f9df0538 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
diff --git a/client/strings/es.json b/client/strings/es.json
index fc2f03164a..4731530158 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minuto",
"LabelMissing": "Ausente",
"LabelMissingParts": "Partes Ausentes",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Más",
"LabelMoreInfo": "Más Información",
"LabelName": "Nombre",
diff --git a/client/strings/fr.json b/client/strings/fr.json
index f10a51f4e5..f6efa428b0 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minute",
"LabelMissing": "Manquant",
"LabelMissingParts": "Parties manquantes",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Plus",
"LabelMoreInfo": "Plus d’info",
"LabelName": "Nom",
diff --git a/client/strings/gu.json b/client/strings/gu.json
index d65bb13e15..0317e2f98d 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
diff --git a/client/strings/hi.json b/client/strings/hi.json
index b172c2e516..eb4f074f9b 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
diff --git a/client/strings/hr.json b/client/strings/hr.json
index 50f384e745..eb7d27d85b 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minuta",
"LabelMissing": "Nedostaje",
"LabelMissingParts": "Nedostajali dijelovi",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Više",
"LabelMoreInfo": "More Info",
"LabelName": "Ime",
diff --git a/client/strings/it.json b/client/strings/it.json
index 638e3468a7..7e5267217a 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minuto",
"LabelMissing": "Altro",
"LabelMissingParts": "Parti rimantenti",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Molto",
"LabelMoreInfo": "Più Info",
"LabelName": "Nome",
diff --git a/client/strings/lt.json b/client/strings/lt.json
index 3e3fda41bf..9c4b9a63c2 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minutė",
"LabelMissing": "Trūksta",
"LabelMissingParts": "Trūkstamos dalys",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Daugiau",
"LabelMoreInfo": "Daugiau informacijos",
"LabelName": "Pavadinimas",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index 0884548871..d4779abd6a 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minuut",
"LabelMissing": "Ontbrekend",
"LabelMissingParts": "Ontbrekende delen",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Meer",
"LabelMoreInfo": "Meer info",
"LabelName": "Naam",
diff --git a/client/strings/no.json b/client/strings/no.json
index 8cbfd91970..511c8b8653 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minutt",
"LabelMissing": "Mangler",
"LabelMissingParts": "Manglende deler",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Mer",
"LabelMoreInfo": "Mer info",
"LabelName": "Navn",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index bf34cbac17..b51084e942 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minuta",
"LabelMissing": "Brakujący",
"LabelMissingParts": "Brakujące cześci",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Więcej",
"LabelMoreInfo": "More Info",
"LabelName": "Nazwa",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index b0ba0f6a74..b48e0dbd20 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -343,6 +343,8 @@
"LabelMinute": "Минуты",
"LabelMissing": "Потеряно",
"LabelMissingParts": "Потерянные части",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Еще",
"LabelMoreInfo": "Больше информации",
"LabelName": "Имя",
diff --git a/client/strings/sv.json b/client/strings/sv.json
index 6883af399f..fde0cd875d 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -343,6 +343,8 @@
"LabelMinute": "Minut",
"LabelMissing": "Saknad",
"LabelMissingParts": "Saknade delar",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "Mer",
"LabelMoreInfo": "Mer information",
"LabelName": "Namn",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 14bfcc0bc4..7c5594897f 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -343,6 +343,8 @@
"LabelMinute": "分钟",
"LabelMissing": "丢失",
"LabelMissingParts": "丢失的部分",
+ "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
+ "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth
, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*
) as the sole entry permits any URI.",
"LabelMore": "更多",
"LabelMoreInfo": "更多..",
"LabelName": "名称",
diff --git a/server/Auth.js b/server/Auth.js
index 267bbb459d..0a282c9c98 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -8,6 +8,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
const OpenIDClient = require('openid-client')
const Database = require('./Database')
const Logger = require('./Logger')
+const e = require('express')
/**
* @class Class for handling all the authentication related functionality.
@@ -15,6 +16,8 @@ const Logger = require('./Logger')
class Auth {
constructor() {
+ // Map of openId sessions indexed by oauth2 state-variable
+ this.openIdAuthSession = new Map()
}
/**
@@ -187,9 +190,10 @@ class Auth {
* @param {import('express').Response} res
*/
paramsToCookies(req, res) {
- if (req.query.isRest?.toLowerCase() == 'true') {
+ // Set if isRest flag is set or if mobile oauth flow is used
+ if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) {
// store the isRest flag to the is_rest cookie
- res.cookie('is_rest', req.query.isRest.toLowerCase(), {
+ res.cookie('is_rest', 'true', {
maxAge: 120000, // 2 min
httpOnly: true
})
@@ -283,8 +287,27 @@ class Auth {
// for API or mobile clients
const oidcStrategy = passport._strategy('openid-client')
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
- oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
- Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
+
+ let mobile_redirect_uri = null
+
+ // The client wishes a different redirect_uri
+ // We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
+ // where we will handle the redirect to it
+ if (req.query.redirect_uri) {
+ // Check if the redirect_uri is in the whitelist
+ if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
+ (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
+ oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
+ mobile_redirect_uri = req.query.redirect_uri
+ } else {
+ Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
+ return res.status(400).send('Invalid redirect_uri')
+ }
+ } else {
+ oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
+ }
+
+ Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
const client = oidcStrategy._client
const sessionKey = oidcStrategy._key
@@ -324,16 +347,21 @@ class Auth {
req.session[sessionKey] = {
...req.session[sessionKey],
...pick(params, 'nonce', 'state', 'max_age', 'response_type'),
- mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later
+ mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
+ sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
}
+ // We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API
+ // for the request to mobile-redirect and as such the session is not shared
+ this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri })
+
// Now get the URL to direct to
const authorizationUrl = client.authorizationUrl({
...params,
scope: 'openid profile email',
response_type: 'code',
code_challenge,
- code_challenge_method,
+ code_challenge_method
})
// params (isRest, callback) to a cookie that will be send to the client
@@ -347,6 +375,37 @@ class Auth {
}
})
+ // This will be the oauth2 callback route for mobile clients
+ // It will redirect to an app-link like audiobookshelf://oauth
+ router.get('/auth/openid/mobile-redirect', (req, res) => {
+ try {
+ // Extract the state parameter from the request
+ const { state, code } = req.query
+
+ // Check if the state provided is in our list
+ if (!state || !this.openIdAuthSession.has(state)) {
+ Logger.error('[Auth] /auth/openid/mobile-redirect route: State parameter mismatch')
+ return res.status(400).send('State parameter mismatch')
+ }
+
+ let mobile_redirect_uri = this.openIdAuthSession.get(state).mobile_redirect_uri
+
+ if (!mobile_redirect_uri) {
+ Logger.error('[Auth] No redirect URI')
+ return res.status(400).send('No redirect URI')
+ }
+
+ this.openIdAuthSession.delete(state)
+
+ const redirectUri = `${mobile_redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
+ // Redirect to the overwrite URI saved in the map
+ res.redirect(redirectUri)
+ } catch (error) {
+ Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`)
+ res.status(500).send('Internal Server Error')
+ }
+ })
+
// openid strategy callback route (this receives the token from the configured openid login provider)
router.get('/auth/openid/callback', (req, res, next) => {
const oidcStrategy = passport._strategy('openid-client')
@@ -403,11 +462,8 @@ class Auth {
// While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
// We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
- if (req.session[sessionKey].mobile) {
- return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next)
- } else {
- return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next)
- }
+ // We set it here again because the passport param can change between requests
+ return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next)
},
// on a successfull login: read the cookies and react like the client requested (callback or json)
this.handleLoginSuccessBasedOnCookie.bind(this))
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index 26a9d77bcd..db4110e099 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -629,6 +629,27 @@ class MiscController {
} else {
Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`)
}
+ } else if (key === 'authOpenIDMobileRedirectURIs') {
+ function isValidRedirectURI(uri) {
+ if (typeof uri !== 'string') return false
+ const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i')
+ return pattern.test(uri)
+ }
+
+ const uris = settingsUpdate[key]
+ if (!Array.isArray(uris) ||
+ (uris.includes('*') && uris.length > 1) ||
+ uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) {
+ Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`)
+ continue
+ }
+
+ // Update the URIs
+ if (Database.serverSettings[key].some(uri => !uris.includes(uri)) || uris.some(uri => !Database.serverSettings[key].includes(uri))) {
+ Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`)
+ Database.serverSettings[key] = uris
+ hasUpdates = true
+ }
} else {
const updatedValueType = typeof settingsUpdate[key]
if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) {
@@ -671,6 +692,7 @@ class MiscController {
}
res.json({
+ updated: hasUpdates,
serverSettings: Database.serverSettings.toJSONForBrowser()
})
}
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index bf3db55771..6e9d84566d 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -71,6 +71,7 @@ class ServerSettings {
this.authOpenIDAutoLaunch = false
this.authOpenIDAutoRegister = false
this.authOpenIDMatchExistingBy = null
+ this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth']
if (settings) {
this.construct(settings)
@@ -126,6 +127,7 @@ class ServerSettings {
this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch
this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister
this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null
+ this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth']
if (!Array.isArray(this.authActiveAuthMethods)) {
this.authActiveAuthMethods = ['local']
@@ -211,7 +213,8 @@ class ServerSettings {
authOpenIDButtonText: this.authOpenIDButtonText,
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
authOpenIDAutoRegister: this.authOpenIDAutoRegister,
- authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy
+ authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
+ authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
}
}
@@ -220,6 +223,7 @@ class ServerSettings {
delete json.tokenSecret
delete json.authOpenIDClientID
delete json.authOpenIDClientSecret
+ delete json.authOpenIDMobileRedirectURIs
return json
}
@@ -254,7 +258,8 @@ class ServerSettings {
authOpenIDButtonText: this.authOpenIDButtonText,
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
authOpenIDAutoRegister: this.authOpenIDAutoRegister,
- authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy
+ authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
+ authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
}
}