From 80fd2a1a1831b415546194fc2e7809a002f85030 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Mon, 4 Dec 2023 22:36:34 +0100 Subject: [PATCH 1/5] SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381) - Implement /auth/openid/mobile-redirect this will redirect to an app-link like audiobookshelf://oauth - An app must provide an `redirect_uri` parameter with the app-link in the authorization request to /auth/openid - The user will have to whitelist possible URLs, or explicitly allow all - Also modified MultiSelect to allow to hide the menu/popup --- client/components/ui/MultiSelect.vue | 8 ++- client/pages/config/authentication.vue | 22 +++++++++ client/strings/de.json | 2 + client/strings/en-us.json | 2 + server/Auth.js | 59 ++++++++++++++++++++++- server/controllers/MiscController.js | 17 +++++++ server/objects/settings/ServerSettings.js | 9 +++- 7 files changed, 114 insertions(+), 5 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 4fa8e394..2009b28d 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 e645569e..ffb1feb7 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -46,6 +46,9 @@ + +

+

@@ -187,6 +190,25 @@ export default { this.$toast.error('Client Secret required') isValid = false } + + function isValidRedirectURI(uri) { + // Check for somestring://someother/string + const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i') + return pattern.test(uri) + } + + const uris = this.newAuthSettings.authOpenIDMobileRedirectURIs + if (uris.includes('*') && uris.length > 1) { + this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used') + isValid = false + } else { + uris.forEach(uri => { + if (uri !== '*' && !isValidRedirectURI(uri)) { + this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`) + isValid = false + } + }) + } return isValid }, async saveSettings() { diff --git a/client/strings/de.json b/client/strings/de.json index 78e64804..eb3d59f4 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -337,6 +337,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 857627e9..02f9df05 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/server/Auth.js b/server/Auth.js index 267bbb45..c20d532a 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() } /** @@ -283,7 +286,26 @@ 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() + + let 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() + 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] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) const client = oidcStrategy._client const sessionKey = oidcStrategy._key @@ -327,6 +349,10 @@ class Auth { mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later } + // 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, { redirect_uri: redirect_uri }) + // Now get the URL to direct to const authorizationUrl = client.authorizationUrl({ ...params, @@ -347,6 +373,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 redirect_uri = this.openIdAuthSession.get(state).redirect_uri + + if (!redirect_uri) { + Logger.error('[Auth] No redirect URI') + return res.status(400).send('No redirect URI') + } + + this.openIdAuthSession.delete(state) + + const redirectUri = `${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') diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 26a9d77b..e209fac9 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -629,6 +629,23 @@ class MiscController { } else { Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) } + } else if (key === 'authOpenIDMobileRedirectURIs') { + function isValidRedirectURI(uri) { + 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 + Database.serverSettings[key] = uris + hasUpdates = true } else { const updatedValueType = typeof settingsUpdate[key] if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index bf3db557..6e9d8456 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 } } From e6ab28365fa740b72295668b924ee5b1d6640f09 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 5 Dec 2023 00:18:58 +0100 Subject: [PATCH 2/5] SSO/OpenID: Remove modifying redirect_uri in the callback The redirect URI will be now correctly set to either /callback or /mobile-redirect in the /auth/openid route --- server/Auth.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index c20d532a..b5bc7d40 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -359,7 +359,7 @@ class Auth { 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 @@ -460,11 +460,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) - } + // This is already done in the strategy in the route to /auth/openid using oidcStrategy._params.redirect_uri + return passport.authenticate('openid-client', 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)) From cf00650c6d3bd74ddb9fae92138c00f808511150 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 5 Dec 2023 09:43:06 +0100 Subject: [PATCH 3/5] SSO/OpenID: Also fix possible race condition - We need to define redirect_uri in the callback again, because the global params of passport can change between calls to the first route (ie. if multiple users log in at same time) - Removed is_rest parameter as requirement for mobile flow (to maximise compatibility with possible oauth libraries) - Also renamed some variables for clarity --- server/Auth.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index b5bc7d40..0a282c9c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -190,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 }) @@ -287,7 +288,7 @@ class Auth { const oidcStrategy = passport._strategy('openid-client') const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' - let redirect_uri = null + 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 @@ -297,7 +298,7 @@ class Auth { 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() - redirect_uri = req.query.redirect_uri + 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') @@ -306,7 +307,7 @@ class Auth { 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}`) + Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) const client = oidcStrategy._client const sessionKey = oidcStrategy._key @@ -346,12 +347,13 @@ 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, { redirect_uri: redirect_uri }) + this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri }) // Now get the URL to direct to const authorizationUrl = client.authorizationUrl({ @@ -386,16 +388,16 @@ class Auth { return res.status(400).send('State parameter mismatch') } - let redirect_uri = this.openIdAuthSession.get(state).redirect_uri + let mobile_redirect_uri = this.openIdAuthSession.get(state).mobile_redirect_uri - if (!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 = `${redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(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) { @@ -460,8 +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 - // This is already done in the strategy in the route to /auth/openid using oidcStrategy._params.redirect_uri - 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)) From 341a0452da4044fe8bec7745d8c54b28a5c5eb6b Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 7 Dec 2023 17:01:33 -0600 Subject: [PATCH 4/5] Update auth settings endpoint to return updated flag and show whether updates were made in client toast --- client/pages/config/authentication.vue | 8 ++++++-- server/controllers/MiscController.js | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index ffb1feb7..9e028307 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -202,7 +202,7 @@ export default { this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used') isValid = false } else { - uris.forEach(uri => { + uris.forEach((uri) => { if (uri !== '*' && !isValidRedirectURI(uri)) { this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`) isValid = false @@ -230,7 +230,11 @@ export default { .$patch('/api/auth-settings', this.newAuthSettings) .then((data) => { this.$store.commit('setServerSettings', data.serverSettings) - this.$toast.success('Server settings updated') + if (data.updated) { + this.$toast.success('Server settings updated') + } else { + this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) + } }) .catch((error) => { console.error('Failed to update server settings', error) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index e209fac9..db4110e0 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -631,21 +631,25 @@ class MiscController { } } else if (key === 'authOpenIDMobileRedirectURIs') { function isValidRedirectURI(uri) { - const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i'); - return pattern.test(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))) { + 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 - Database.serverSettings[key] = uris - hasUpdates = true + 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)) { @@ -688,6 +692,7 @@ class MiscController { } res.json({ + updated: hasUpdates, serverSettings: Database.serverSettings.toJSONForBrowser() }) } From 98104a3c03591af2c8b8885631ce5bf87c556682 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 7 Dec 2023 17:05:52 -0600 Subject: [PATCH 5/5] Map new translations to other files --- client/strings/cs.json | 2 ++ client/strings/da.json | 2 ++ client/strings/es.json | 2 ++ client/strings/fr.json | 2 ++ client/strings/gu.json | 2 ++ client/strings/hi.json | 2 ++ client/strings/hr.json | 2 ++ client/strings/it.json | 2 ++ client/strings/lt.json | 2 ++ client/strings/nl.json | 2 ++ client/strings/no.json | 2 ++ client/strings/pl.json | 2 ++ client/strings/ru.json | 2 ++ client/strings/sv.json | 2 ++ client/strings/zh-cn.json | 2 ++ 15 files changed, 30 insertions(+) diff --git a/client/strings/cs.json b/client/strings/cs.json index b8936024..6d39569e 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuta", "LabelMissing": "Chybějící", "LabelMissingParts": "Chybějící díly", + "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": "Více", "LabelMoreInfo": "Více informací", "LabelName": "Jméno", diff --git a/client/strings/da.json b/client/strings/da.json index a93507c0..fa28dd24 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/es.json b/client/strings/es.json index fc2f0316..47315301 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 f10a51f4..f6efa428 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 d65bb13e..0317e2f9 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 b172c2e5..eb4f074f 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 50f384e7..eb7d27d8 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 638e3468..7e526721 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 3e3fda41..9c4b9a63 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 08845488..d4779abd 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 8cbfd919..511c8b86 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 bf34cbac..b51084e9 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 b0ba0f6a..b48e0dbd 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 6883af39..fde0cd87 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 14bfcc0b..7c559489 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": "名称",