mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #2386 from Sapd/sso-redirecturi
SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381)
This commit is contained in:
		
						commit
						b8c8d2a02e
					
				| @ -50,7 +50,11 @@ export default { | |||||||
|     label: String, |     label: String, | ||||||
|     disabled: Boolean, |     disabled: Boolean, | ||||||
|     readonly: Boolean, |     readonly: Boolean, | ||||||
|     showEdit: Boolean |     showEdit: Boolean, | ||||||
|  |     menuDisabled: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @ -77,7 +81,7 @@ export default { | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     showMenu() { |     showMenu() { | ||||||
|       return this.isFocused |       return this.isFocused && !this.menuDisabled | ||||||
|     }, |     }, | ||||||
|     wrapperClass() { |     wrapperClass() { | ||||||
|       var classes = [] |       var classes = [] | ||||||
|  | |||||||
| @ -46,6 +46,9 @@ | |||||||
| 
 | 
 | ||||||
|             <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> |             <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> | ||||||
| 
 | 
 | ||||||
|  |             <ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" /> | ||||||
|  |             <p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> | ||||||
|  | 
 | ||||||
|             <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> |             <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> | ||||||
| 
 | 
 | ||||||
|             <div class="flex items-center pt-1 mb-2"> |             <div class="flex items-center pt-1 mb-2"> | ||||||
| @ -187,6 +190,25 @@ export default { | |||||||
|         this.$toast.error('Client Secret required') |         this.$toast.error('Client Secret required') | ||||||
|         isValid = false |         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 |       return isValid | ||||||
|     }, |     }, | ||||||
|     async saveSettings() { |     async saveSettings() { | ||||||
| @ -208,7 +230,11 @@ export default { | |||||||
|         .$patch('/api/auth-settings', this.newAuthSettings) |         .$patch('/api/auth-settings', this.newAuthSettings) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|           this.$store.commit('setServerSettings', data.serverSettings) |           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) => { |         .catch((error) => { | ||||||
|           console.error('Failed to update server settings', error) |           console.error('Failed to update server settings', error) | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuta", |   "LabelMinute": "Minuta", | ||||||
|   "LabelMissing": "Chybějící", |   "LabelMissing": "Chybějící", | ||||||
|   "LabelMissingParts": "Chybějící díly", |   "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 <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Více", |   "LabelMore": "Více", | ||||||
|   "LabelMoreInfo": "Více informací", |   "LabelMoreInfo": "Více informací", | ||||||
|   "LabelName": "Jméno", |   "LabelName": "Jméno", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minut", |   "LabelMinute": "Minut", | ||||||
|   "LabelMissing": "Mangler", |   "LabelMissing": "Mangler", | ||||||
|   "LabelMissingParts": "Manglende dele", |   "LabelMissingParts": "Manglende dele", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Mere", |   "LabelMore": "Mere", | ||||||
|   "LabelMoreInfo": "Mere info", |   "LabelMoreInfo": "Mere info", | ||||||
|   "LabelName": "Navn", |   "LabelName": "Navn", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minute", |   "LabelMinute": "Minute", | ||||||
|   "LabelMissing": "Fehlend", |   "LabelMissing": "Fehlend", | ||||||
|   "LabelMissingParts": "Fehlende Teile", |   "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 <code>audiobookshelf://oauth</code>, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", | ||||||
|   "LabelMore": "Mehr", |   "LabelMore": "Mehr", | ||||||
|   "LabelMoreInfo": "Mehr Info", |   "LabelMoreInfo": "Mehr Info", | ||||||
|   "LabelName": "Name", |   "LabelName": "Name", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minute", |   "LabelMinute": "Minute", | ||||||
|   "LabelMissing": "Missing", |   "LabelMissing": "Missing", | ||||||
|   "LabelMissingParts": "Missing Parts", |   "LabelMissingParts": "Missing Parts", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "More", |   "LabelMore": "More", | ||||||
|   "LabelMoreInfo": "More Info", |   "LabelMoreInfo": "More Info", | ||||||
|   "LabelName": "Name", |   "LabelName": "Name", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuto", |   "LabelMinute": "Minuto", | ||||||
|   "LabelMissing": "Ausente", |   "LabelMissing": "Ausente", | ||||||
|   "LabelMissingParts": "Partes Ausentes", |   "LabelMissingParts": "Partes Ausentes", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Más", |   "LabelMore": "Más", | ||||||
|   "LabelMoreInfo": "Más Información", |   "LabelMoreInfo": "Más Información", | ||||||
|   "LabelName": "Nombre", |   "LabelName": "Nombre", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minute", |   "LabelMinute": "Minute", | ||||||
|   "LabelMissing": "Manquant", |   "LabelMissing": "Manquant", | ||||||
|   "LabelMissingParts": "Parties manquantes", |   "LabelMissingParts": "Parties manquantes", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Plus", |   "LabelMore": "Plus", | ||||||
|   "LabelMoreInfo": "Plus d’info", |   "LabelMoreInfo": "Plus d’info", | ||||||
|   "LabelName": "Nom", |   "LabelName": "Nom", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minute", |   "LabelMinute": "Minute", | ||||||
|   "LabelMissing": "Missing", |   "LabelMissing": "Missing", | ||||||
|   "LabelMissingParts": "Missing Parts", |   "LabelMissingParts": "Missing Parts", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "More", |   "LabelMore": "More", | ||||||
|   "LabelMoreInfo": "More Info", |   "LabelMoreInfo": "More Info", | ||||||
|   "LabelName": "Name", |   "LabelName": "Name", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minute", |   "LabelMinute": "Minute", | ||||||
|   "LabelMissing": "Missing", |   "LabelMissing": "Missing", | ||||||
|   "LabelMissingParts": "Missing Parts", |   "LabelMissingParts": "Missing Parts", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "More", |   "LabelMore": "More", | ||||||
|   "LabelMoreInfo": "More Info", |   "LabelMoreInfo": "More Info", | ||||||
|   "LabelName": "Name", |   "LabelName": "Name", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuta", |   "LabelMinute": "Minuta", | ||||||
|   "LabelMissing": "Nedostaje", |   "LabelMissing": "Nedostaje", | ||||||
|   "LabelMissingParts": "Nedostajali dijelovi", |   "LabelMissingParts": "Nedostajali dijelovi", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Više", |   "LabelMore": "Više", | ||||||
|   "LabelMoreInfo": "More Info", |   "LabelMoreInfo": "More Info", | ||||||
|   "LabelName": "Ime", |   "LabelName": "Ime", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuto", |   "LabelMinute": "Minuto", | ||||||
|   "LabelMissing": "Altro", |   "LabelMissing": "Altro", | ||||||
|   "LabelMissingParts": "Parti rimantenti", |   "LabelMissingParts": "Parti rimantenti", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Molto", |   "LabelMore": "Molto", | ||||||
|   "LabelMoreInfo": "Più Info", |   "LabelMoreInfo": "Più Info", | ||||||
|   "LabelName": "Nome", |   "LabelName": "Nome", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minutė", |   "LabelMinute": "Minutė", | ||||||
|   "LabelMissing": "Trūksta", |   "LabelMissing": "Trūksta", | ||||||
|   "LabelMissingParts": "Trūkstamos dalys", |   "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 <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Daugiau", |   "LabelMore": "Daugiau", | ||||||
|   "LabelMoreInfo": "Daugiau informacijos", |   "LabelMoreInfo": "Daugiau informacijos", | ||||||
|   "LabelName": "Pavadinimas", |   "LabelName": "Pavadinimas", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuut", |   "LabelMinute": "Minuut", | ||||||
|   "LabelMissing": "Ontbrekend", |   "LabelMissing": "Ontbrekend", | ||||||
|   "LabelMissingParts": "Ontbrekende delen", |   "LabelMissingParts": "Ontbrekende delen", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Meer", |   "LabelMore": "Meer", | ||||||
|   "LabelMoreInfo": "Meer info", |   "LabelMoreInfo": "Meer info", | ||||||
|   "LabelName": "Naam", |   "LabelName": "Naam", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minutt", |   "LabelMinute": "Minutt", | ||||||
|   "LabelMissing": "Mangler", |   "LabelMissing": "Mangler", | ||||||
|   "LabelMissingParts": "Manglende deler", |   "LabelMissingParts": "Manglende deler", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Mer", |   "LabelMore": "Mer", | ||||||
|   "LabelMoreInfo": "Mer info", |   "LabelMoreInfo": "Mer info", | ||||||
|   "LabelName": "Navn", |   "LabelName": "Navn", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minuta", |   "LabelMinute": "Minuta", | ||||||
|   "LabelMissing": "Brakujący", |   "LabelMissing": "Brakujący", | ||||||
|   "LabelMissingParts": "Brakujące cześci", |   "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 <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Więcej", |   "LabelMore": "Więcej", | ||||||
|   "LabelMoreInfo": "More Info", |   "LabelMoreInfo": "More Info", | ||||||
|   "LabelName": "Nazwa", |   "LabelName": "Nazwa", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Минуты", |   "LabelMinute": "Минуты", | ||||||
|   "LabelMissing": "Потеряно", |   "LabelMissing": "Потеряно", | ||||||
|   "LabelMissingParts": "Потерянные части", |   "LabelMissingParts": "Потерянные части", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Еще", |   "LabelMore": "Еще", | ||||||
|   "LabelMoreInfo": "Больше информации", |   "LabelMoreInfo": "Больше информации", | ||||||
|   "LabelName": "Имя", |   "LabelName": "Имя", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "Minut", |   "LabelMinute": "Minut", | ||||||
|   "LabelMissing": "Saknad", |   "LabelMissing": "Saknad", | ||||||
|   "LabelMissingParts": "Saknade delar", |   "LabelMissingParts": "Saknade delar", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "Mer", |   "LabelMore": "Mer", | ||||||
|   "LabelMoreInfo": "Mer information", |   "LabelMoreInfo": "Mer information", | ||||||
|   "LabelName": "Namn", |   "LabelName": "Namn", | ||||||
|  | |||||||
| @ -343,6 +343,8 @@ | |||||||
|   "LabelMinute": "分钟", |   "LabelMinute": "分钟", | ||||||
|   "LabelMissing": "丢失", |   "LabelMissing": "丢失", | ||||||
|   "LabelMissingParts": "丢失的部分", |   "LabelMissingParts": "丢失的部分", | ||||||
|  |   "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", | ||||||
|  |   "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", | ||||||
|   "LabelMore": "更多", |   "LabelMore": "更多", | ||||||
|   "LabelMoreInfo": "更多..", |   "LabelMoreInfo": "更多..", | ||||||
|   "LabelName": "名称", |   "LabelName": "名称", | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt | |||||||
| const OpenIDClient = require('openid-client') | const OpenIDClient = require('openid-client') | ||||||
| const Database = require('./Database') | const Database = require('./Database') | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
|  | const e = require('express') | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @class Class for handling all the authentication related functionality. |  * @class Class for handling all the authentication related functionality. | ||||||
| @ -15,6 +16,8 @@ const Logger = require('./Logger') | |||||||
| class Auth { | class Auth { | ||||||
| 
 | 
 | ||||||
|   constructor() { |   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 |    * @param {import('express').Response} res | ||||||
|    */ |    */ | ||||||
|   paramsToCookies(req, 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 
 |       // 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
 |         maxAge: 120000, // 2 min
 | ||||||
|         httpOnly: true |         httpOnly: true | ||||||
|       }) |       }) | ||||||
| @ -283,8 +287,27 @@ class Auth { | |||||||
|         //    for API or mobile clients
 |         //    for API or mobile clients
 | ||||||
|         const oidcStrategy = passport._strategy('openid-client') |         const oidcStrategy = passport._strategy('openid-client') | ||||||
|         const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' |         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 client = oidcStrategy._client | ||||||
|         const sessionKey = oidcStrategy._key |         const sessionKey = oidcStrategy._key | ||||||
| 
 | 
 | ||||||
| @ -324,16 +347,21 @@ class Auth { | |||||||
|         req.session[sessionKey] = { |         req.session[sessionKey] = { | ||||||
|           ...req.session[sessionKey], |           ...req.session[sessionKey], | ||||||
|           ...pick(params, 'nonce', 'state', 'max_age', 'response_type'), |           ...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
 |         // Now get the URL to direct to
 | ||||||
|         const authorizationUrl = client.authorizationUrl({ |         const authorizationUrl = client.authorizationUrl({ | ||||||
|           ...params, |           ...params, | ||||||
|           scope: 'openid profile email', |           scope: 'openid profile email', | ||||||
|           response_type: 'code', |           response_type: 'code', | ||||||
|           code_challenge, |           code_challenge, | ||||||
|           code_challenge_method, |           code_challenge_method | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         // params (isRest, callback) to a cookie that will be send to the client
 |         // 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)
 |     // openid strategy callback route (this receives the token from the configured openid login provider)
 | ||||||
|     router.get('/auth/openid/callback', (req, res, next) => { |     router.get('/auth/openid/callback', (req, res, next) => { | ||||||
|       const oidcStrategy = passport._strategy('openid-client') |       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
 |       // 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
 |       // 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) { |       // We set it here again because the passport param can change between requests
 | ||||||
|         return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next) |       return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next) | ||||||
|       } else { |  | ||||||
|         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)
 |       // on a successfull login: read the cookies and react like the client requested (callback or json)
 | ||||||
|       this.handleLoginSuccessBasedOnCookie.bind(this)) |       this.handleLoginSuccessBasedOnCookie.bind(this)) | ||||||
|  | |||||||
| @ -629,6 +629,27 @@ class MiscController { | |||||||
|         } else { |         } else { | ||||||
|           Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) |           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 { |       } else { | ||||||
|         const updatedValueType = typeof settingsUpdate[key] |         const updatedValueType = typeof settingsUpdate[key] | ||||||
|         if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { |         if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { | ||||||
| @ -671,6 +692,7 @@ class MiscController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|  |       updated: hasUpdates, | ||||||
|       serverSettings: Database.serverSettings.toJSONForBrowser() |       serverSettings: Database.serverSettings.toJSONForBrowser() | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -71,6 +71,7 @@ class ServerSettings { | |||||||
|     this.authOpenIDAutoLaunch = false |     this.authOpenIDAutoLaunch = false | ||||||
|     this.authOpenIDAutoRegister = false |     this.authOpenIDAutoRegister = false | ||||||
|     this.authOpenIDMatchExistingBy = null |     this.authOpenIDMatchExistingBy = null | ||||||
|  |     this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] | ||||||
| 
 | 
 | ||||||
|     if (settings) { |     if (settings) { | ||||||
|       this.construct(settings) |       this.construct(settings) | ||||||
| @ -126,6 +127,7 @@ class ServerSettings { | |||||||
|     this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch |     this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch | ||||||
|     this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister |     this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister | ||||||
|     this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null |     this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null | ||||||
|  |     this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] | ||||||
| 
 | 
 | ||||||
|     if (!Array.isArray(this.authActiveAuthMethods)) { |     if (!Array.isArray(this.authActiveAuthMethods)) { | ||||||
|       this.authActiveAuthMethods = ['local'] |       this.authActiveAuthMethods = ['local'] | ||||||
| @ -211,7 +213,8 @@ class ServerSettings { | |||||||
|       authOpenIDButtonText: this.authOpenIDButtonText, |       authOpenIDButtonText: this.authOpenIDButtonText, | ||||||
|       authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, |       authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, | ||||||
|       authOpenIDAutoRegister: this.authOpenIDAutoRegister, |       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.tokenSecret | ||||||
|     delete json.authOpenIDClientID |     delete json.authOpenIDClientID | ||||||
|     delete json.authOpenIDClientSecret |     delete json.authOpenIDClientSecret | ||||||
|  |     delete json.authOpenIDMobileRedirectURIs | ||||||
|     return json |     return json | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -254,7 +258,8 @@ class ServerSettings { | |||||||
|       authOpenIDButtonText: this.authOpenIDButtonText, |       authOpenIDButtonText: this.authOpenIDButtonText, | ||||||
|       authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, |       authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, | ||||||
|       authOpenIDAutoRegister: this.authOpenIDAutoRegister, |       authOpenIDAutoRegister: this.authOpenIDAutoRegister, | ||||||
|       authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy |       authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, | ||||||
|  |       authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
 | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user