mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-28 21:19:42 +00:00
Implement OIDC Back-Channel Logout 1.0 (RFC). When enabled, the IdP can POST a signed logout_token JWT to invalidate user sessions server-side. - Add BackchannelLogoutHandler: JWT verification via jose, jti replay protection with bounded cache, session destruction by sub or sid - Add oidcSessionId column to sessions table with index for fast lookups - Add backchannel logout route (POST /auth/openid/backchannel-logout) - Notify connected clients via socket to redirect to login page - Add authOpenIDBackchannelLogoutEnabled toggle in schema-driven settings UI - Migration v2.34.0 adds oidcSessionId column and index - Polish settings UI: auto-populate loading state, subfolder dropdown options, KeyValueEditor fixes, localized descriptions via descriptionKey, duplicate key detection, success/error toasts - Localize backchannel logout toast (ToastSessionEndedByProvider) - OidcAuthStrategy tests now use real class via require-cache stubbing
224 lines
7.8 KiB
Vue
224 lines
7.8 KiB
Vue
<template>
|
|
<div id="authentication-settings">
|
|
<app-settings-content :header-text="$strings.HeaderAuthentication">
|
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
|
<div class="flex items-center">
|
|
<ui-checkbox v-model="showCustomLoginMessage" checkbox-bg="bg" />
|
|
<p class="text-lg pl-4">{{ $strings.HeaderCustomMessageOnLogin }}</p>
|
|
</div>
|
|
<transition name="slide">
|
|
<div v-if="showCustomLoginMessage" class="w-full pt-4">
|
|
<ui-rich-text-editor v-model="newAuthSettings.authLoginCustomMessage" />
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
|
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
|
<div class="flex items-center">
|
|
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
|
|
<p class="text-lg pl-4">{{ $strings.HeaderPasswordAuthentication }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
|
<div class="flex items-center">
|
|
<ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" />
|
|
<p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p>
|
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
|
<a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex">
|
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
|
</a>
|
|
</ui-tooltip>
|
|
</div>
|
|
|
|
<transition name="slide">
|
|
<div v-if="enableOpenIDAuth" class="pt-4">
|
|
<app-oidc-settings :schema="openIDSchema" :groups="openIDGroups" :values="openIDValues" :schema-overrides="openIDSchemaOverrides" :disabled="savingSettings" :loading-actions="discovering ? ['discover'] : []" @update="onOidcSettingChange" @action="onOidcAction" />
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
<div class="w-full flex items-center justify-end p-4">
|
|
<ui-btn color="bg-success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
|
|
</div>
|
|
</app-settings-content>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
async asyncData({ store, redirect, app }) {
|
|
if (!store.getters['user/getIsAdminOrUp']) {
|
|
redirect('/')
|
|
return
|
|
}
|
|
|
|
const authSettings = await app.$axios.$get('/api/auth-settings').catch((error) => {
|
|
console.error('Failed', error)
|
|
return null
|
|
})
|
|
if (!authSettings) {
|
|
redirect('/config')
|
|
return
|
|
}
|
|
return {
|
|
authSettings
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
enableLocalAuth: false,
|
|
enableOpenIDAuth: false,
|
|
showCustomLoginMessage: false,
|
|
savingSettings: false,
|
|
discovering: false,
|
|
openIDSchemaOverrides: {},
|
|
newAuthSettings: {},
|
|
openIDValues: {}
|
|
}
|
|
},
|
|
computed: {
|
|
authMethods() {
|
|
return this.authSettings.authActiveAuthMethods || []
|
|
},
|
|
openIDSchema() {
|
|
return this.authSettings.openIDSettings?.schema || []
|
|
},
|
|
openIDGroups() {
|
|
return this.authSettings.openIDSettings?.groups || []
|
|
}
|
|
},
|
|
methods: {
|
|
onOidcSettingChange({ key, value }) {
|
|
this.$set(this.openIDValues, key, value)
|
|
},
|
|
onOidcAction(action) {
|
|
if (action === 'discover') {
|
|
this.discoverOIDC()
|
|
}
|
|
},
|
|
async discoverOIDC() {
|
|
let issuerUrl = this.openIDValues.authOpenIDIssuerURL
|
|
if (!issuerUrl) {
|
|
this.$toast.error('Issuer URL required')
|
|
return
|
|
}
|
|
|
|
// Remove trailing slash
|
|
if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1)
|
|
|
|
// If the full config path is on the issuer url then remove it
|
|
if (issuerUrl.endsWith('/.well-known/openid-configuration')) {
|
|
issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '')
|
|
this.$set(this.openIDValues, 'authOpenIDIssuerURL', issuerUrl)
|
|
}
|
|
|
|
this.discovering = true
|
|
try {
|
|
const data = await this.$axios.$post('/api/auth-settings/openid/discover', { issuerUrl })
|
|
|
|
// Apply discovered values
|
|
if (data.values) {
|
|
for (const [key, value] of Object.entries(data.values)) {
|
|
if (value !== null && value !== undefined) {
|
|
this.$set(this.openIDValues, key, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge schema overrides (e.g., supported signing algorithms) with existing ones
|
|
if (data.schemaOverrides) {
|
|
this.openIDSchemaOverrides = { ...this.openIDSchemaOverrides, ...data.schemaOverrides }
|
|
}
|
|
|
|
this.$toast.success('Provider endpoints auto-populated')
|
|
} catch (error) {
|
|
console.error('Failed to discover OIDC config', error)
|
|
const errorMsg = error.response?.data?.error || error.response?.data || 'Unknown error'
|
|
this.$toast.error(errorMsg)
|
|
} finally {
|
|
this.discovering = false
|
|
}
|
|
},
|
|
async saveSettings() {
|
|
if (!this.enableLocalAuth && !this.enableOpenIDAuth) {
|
|
this.$toast.error('Must have at least one authentication method enabled')
|
|
return
|
|
}
|
|
|
|
if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) {
|
|
this.newAuthSettings.authLoginCustomMessage = null
|
|
}
|
|
|
|
const authActiveAuthMethods = []
|
|
if (this.enableLocalAuth) authActiveAuthMethods.push('local')
|
|
if (this.enableOpenIDAuth) authActiveAuthMethods.push('openid')
|
|
|
|
const payload = {
|
|
authLoginCustomMessage: this.newAuthSettings.authLoginCustomMessage,
|
|
authActiveAuthMethods,
|
|
openIDSettings: this.openIDValues
|
|
}
|
|
|
|
this.savingSettings = true
|
|
try {
|
|
const data = await this.$axios.$patch('/api/auth-settings', payload)
|
|
this.$store.commit('setServerSettings', data.serverSettings)
|
|
if (data.updated) {
|
|
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
|
|
} else {
|
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update server settings', error)
|
|
if (error.response?.data?.details) {
|
|
error.response.data.details.forEach((detail) => this.$toast.error(detail))
|
|
} else {
|
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
|
}
|
|
} finally {
|
|
this.savingSettings = false
|
|
}
|
|
},
|
|
init() {
|
|
this.newAuthSettings = {
|
|
authLoginCustomMessage: this.authSettings.authLoginCustomMessage,
|
|
authActiveAuthMethods: this.authSettings.authActiveAuthMethods
|
|
}
|
|
|
|
// Initialize OIDC values from server response
|
|
const serverValues = this.authSettings.openIDSettings?.values || {}
|
|
this.openIDValues = {
|
|
...serverValues,
|
|
authOpenIDSubfolderForRedirectURLs: serverValues.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : serverValues.authOpenIDSubfolderForRedirectURLs
|
|
}
|
|
|
|
// Build subfolder dropdown options from routerBasePath
|
|
const basePath = this.$config.routerBasePath
|
|
const subfolderOptions = [{ value: '', label: 'None' }]
|
|
if (basePath && basePath !== '/') {
|
|
subfolderOptions.push({ value: basePath, label: basePath })
|
|
}
|
|
this.openIDSchemaOverrides = {
|
|
authOpenIDSubfolderForRedirectURLs: { options: subfolderOptions }
|
|
}
|
|
|
|
this.enableLocalAuth = this.authMethods.includes('local')
|
|
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
|
this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage
|
|
}
|
|
},
|
|
mounted() {
|
|
this.init()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
#authentication-settings code {
|
|
font-size: 0.8rem;
|
|
border-radius: 6px;
|
|
background-color: rgb(82, 82, 82);
|
|
color: white;
|
|
padding: 2px 4px;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|