Force re-login if using old token, show alert if admin user, add isOldToken flag to user
Some checks are pending
Run Component Tests / Run Component Tests (push) Waiting to run
Integration Test / build and test (push) Waiting to run
Run Unit Tests / Run Unit Tests (push) Waiting to run

This commit is contained in:
advplyr 2025-07-05 17:46:18 -05:00
parent 8dbe1e4e5d
commit e59babdf24
3 changed files with 44 additions and 3 deletions

View file

@ -40,6 +40,15 @@
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<div v-if="showNewAuthSystemAdminMessage" class="mb-4">
<widgets-alert type="warning">
<div>
<p>Authentication has been improved for security. All users will be required to re-login.</p>
<a href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">More info</a>
</div>
</widgets-alert>
</div>
<form v-show="login_local" @submit.prevent="submitForm"> <form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" /> <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
@ -85,7 +94,8 @@ export default {
MetadataPath: '', MetadataPath: '',
login_local: true, login_local: true,
login_openid: false, login_openid: false,
authFormData: null authFormData: null,
showNewAuthSystemAdminMessage: false
} }
}, },
watch: { watch: {
@ -184,6 +194,7 @@ export default {
}, },
async submitForm() { async submitForm() {
this.error = null this.error = null
this.showNewAuthSystemAdminMessage = false
this.processing = true this.processing = true
const payload = { const payload = {
@ -217,15 +228,28 @@ export default {
} }
}) })
.then((res) => { .then((res) => {
// Force re-login if user is using an old token with no expiration
if (res.user.isOldToken) {
if (res.user.type === 'admin' || res.user.type === 'root') {
this.username = res.user.username
// Show message to admin users about new auth system
this.showNewAuthSystemAdminMessage = true
} else {
// Regular users just shown login
this.username = res.user.username
}
return false
}
this.setUser(res) this.setUser(res)
this.processing = false
return true return true
}) })
.catch((error) => { .catch((error) => {
console.error('Authorize error', error) console.error('Authorize error', error)
this.processing = false
return false return false
}) })
.finally(() => {
this.processing = false
})
}, },
checkStatus() { checkStatus() {
this.processing = true this.processing = true

View file

@ -492,6 +492,7 @@ class Auth {
} }
if (!refreshToken) { if (!refreshToken) {
Logger.error(`[Auth] Failed to refresh token. No refresh token provided`)
return res.status(401).json({ error: 'No refresh token provided' }) return res.status(401).json({ error: 'No refresh token provided' })
} }
@ -502,6 +503,7 @@ class Auth {
const decoded = jwt.verify(refreshToken, global.ServerSettings.tokenSecret) const decoded = jwt.verify(refreshToken, global.ServerSettings.tokenSecret)
if (decoded.type !== 'refresh') { if (decoded.type !== 'refresh') {
Logger.error(`[Auth] Failed to refresh token. Invalid token type: ${decoded.type}`)
return res.status(401).json({ error: 'Invalid token type' }) return res.status(401).json({ error: 'Invalid token type' })
} }
@ -510,6 +512,7 @@ class Auth {
}) })
if (!session) { if (!session) {
Logger.error(`[Auth] Failed to refresh token. Session not found for refresh token: ${refreshToken}`)
return res.status(401).json({ error: 'Invalid refresh token' }) return res.status(401).json({ error: 'Invalid refresh token' })
} }
@ -522,6 +525,7 @@ class Auth {
const user = await Database.userModel.getUserById(decoded.userId) const user = await Database.userModel.getUserById(decoded.userId)
if (!user?.isActive) { if (!user?.isActive) {
Logger.error(`[Auth] Failed to refresh token. User not found or inactive for user id: ${decoded.userId}`)
return res.status(401).json({ error: 'User not found or inactive' }) return res.status(401).json({ error: 'User not found or inactive' })
} }
@ -1128,6 +1132,16 @@ class Auth {
done(null, null) done(null, null)
return return
} }
// TODO: Temporary flag to report old tokens to users
// May be a better place for this but here means we dont have to decode the token again
if (!jwt_payload.exp && !user.isOldToken) {
Logger.debug(`[Auth] User ${user.username} is using an access token without an expiration`)
user.isOldToken = true
} else if (jwt_payload.exp && user.isOldToken !== undefined) {
delete user.isOldToken
}
// approve login // approve login
done(null, user) done(null, user)
} }

View file

@ -522,6 +522,9 @@ class User extends Model {
type: this.type, type: this.type,
// TODO: Old non-expiring token // TODO: Old non-expiring token
token: this.type === 'root' && hideRootToken ? '' : this.token, token: this.type === 'root' && hideRootToken ? '' : this.token,
// TODO: Temporary flag not saved in db that is set in Auth.js jwtAuthCheck
// Necessary to detect apps using old tokens that no longer match the old token stored on the user
isOldToken: this.isOldToken,
mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [], mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
seriesHideFromContinueListening: [...seriesHideFromContinueListening], seriesHideFromContinueListening: [...seriesHideFromContinueListening],
bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [], bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],