mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 10:55:05 +02:00
Add support for returning refresh token for mobile clients
Some checks failed
Some checks failed
This commit is contained in:
parent
4d32a22de9
commit
8b995a179d
4 changed files with 35 additions and 14 deletions
|
@ -152,8 +152,11 @@ export const mutations = {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Use accessToken from user if included in response (for login)
|
||||||
if (user.accessToken) localStorage.setItem('token', user.accessToken)
|
if (user.accessToken) localStorage.setItem('token', user.accessToken)
|
||||||
else {
|
else if (localStorage.getItem('token')) {
|
||||||
|
user.accessToken = localStorage.getItem('token')
|
||||||
|
} else {
|
||||||
console.error('No access token found for user', user)
|
console.error('No access token found for user', user)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -466,14 +466,29 @@ class Auth {
|
||||||
// return the user login response json if the login was successfull
|
// return the user login response json if the login was successfull
|
||||||
const userResponse = await this.getUserLoginResponsePayload(req.user)
|
const userResponse = await this.getUserLoginResponsePayload(req.user)
|
||||||
|
|
||||||
this.setRefreshTokenCookie(req, res, req.user.refreshToken)
|
// Check if mobile app wants refresh token in response
|
||||||
|
const returnTokens = req.query.return_tokens === 'true' || req.headers['x-return-tokens'] === 'true'
|
||||||
|
|
||||||
|
userResponse.user.refreshToken = returnTokens ? req.user.refreshToken : null
|
||||||
|
userResponse.user.accessToken = req.user.accessToken
|
||||||
|
|
||||||
|
if (!returnTokens) {
|
||||||
|
this.setRefreshTokenCookie(req, res, req.user.refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Refresh token route
|
// Refresh token route
|
||||||
router.post('/auth/refresh', async (req, res) => {
|
router.post('/auth/refresh', async (req, res) => {
|
||||||
const refreshToken = req.cookies.refresh_token
|
let refreshToken = req.cookies.refresh_token
|
||||||
|
|
||||||
|
// For mobile clients, the refresh token is sent in the authorization header
|
||||||
|
let shouldReturnRefreshToken = false
|
||||||
|
if (!refreshToken && req.headers.authorization?.startsWith('Bearer ')) {
|
||||||
|
refreshToken = req.headers.authorization.split(' ')[1]
|
||||||
|
shouldReturnRefreshToken = true
|
||||||
|
}
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return res.status(401).json({ error: 'No refresh token provided' })
|
return res.status(401).json({ error: 'No refresh token provided' })
|
||||||
|
@ -507,10 +522,12 @@ class Auth {
|
||||||
return res.status(401).json({ error: 'User not found or inactive' })
|
return res.status(401).json({ error: 'User not found or inactive' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAccessToken = await this.rotateTokensForSession(session, user, req, res)
|
const newTokens = await this.rotateTokensForSession(session, user, req, res)
|
||||||
|
|
||||||
user.accessToken = newAccessToken
|
|
||||||
const userResponse = await this.getUserLoginResponsePayload(user)
|
const userResponse = await this.getUserLoginResponsePayload(user)
|
||||||
|
|
||||||
|
userResponse.user.accessToken = newTokens.accessToken
|
||||||
|
userResponse.user.refreshToken = shouldReturnRefreshToken ? newTokens.refreshToken : null
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'TokenExpiredError') {
|
if (error.name === 'TokenExpiredError') {
|
||||||
|
@ -961,7 +978,7 @@ class Auth {
|
||||||
* @param {import('./models/User')} user
|
* @param {import('./models/User')} user
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @returns {Promise<string>} newAccessToken
|
* @returns {Promise<{ accessToken:string, refreshToken:string }>}
|
||||||
*/
|
*/
|
||||||
async rotateTokensForSession(session, user, req, res) {
|
async rotateTokensForSession(session, user, req, res) {
|
||||||
// Generate new tokens
|
// Generate new tokens
|
||||||
|
@ -978,7 +995,10 @@ class Auth {
|
||||||
// Set new refresh token cookie
|
// Set new refresh token cookie
|
||||||
this.setRefreshTokenCookie(req, res, newRefreshToken)
|
this.setRefreshTokenCookie(req, res, newRefreshToken)
|
||||||
|
|
||||||
return newAccessToken
|
return {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -996,7 +1016,7 @@ class Auth {
|
||||||
// So rotate token for current session
|
// So rotate token for current session
|
||||||
const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } })
|
const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } })
|
||||||
if (currentSession) {
|
if (currentSession) {
|
||||||
const newAccessToken = await this.rotateTokensForSession(currentSession, user, req, res)
|
const newTokens = await this.rotateTokensForSession(currentSession, user, req, res)
|
||||||
|
|
||||||
// Invalidate all sessions for the user except the current one
|
// Invalidate all sessions for the user except the current one
|
||||||
await Database.sessionModel.destroy({
|
await Database.sessionModel.destroy({
|
||||||
|
@ -1008,7 +1028,7 @@ class Auth {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return newAccessToken
|
return newTokens.accessToken
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[Auth] No session found to rotate tokens for refresh token ${currentRefreshToken}`)
|
Logger.error(`[Auth] No session found to rotate tokens for refresh token ${currentRefreshToken}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,6 +336,9 @@ class UserController {
|
||||||
const newAccessToken = await this.auth.invalidateJwtSessionsForUser(user, req, res)
|
const newAccessToken = await this.auth.invalidateJwtSessionsForUser(user, req, res)
|
||||||
if (newAccessToken) {
|
if (newAccessToken) {
|
||||||
user.accessToken = newAccessToken
|
user.accessToken = newAccessToken
|
||||||
|
// Refresh tokens are only returned for mobile clients
|
||||||
|
// Mobile apps currently do not use this API endpoint so always set to null
|
||||||
|
user.refreshToken = null
|
||||||
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username} and rotated tokens for current session`)
|
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username} and rotated tokens for current session`)
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username}`)
|
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username}`)
|
||||||
|
|
|
@ -112,10 +112,6 @@ class User extends Model {
|
||||||
this.updatedAt
|
this.updatedAt
|
||||||
/** @type {import('./MediaProgress')[]?} - Only included when extended */
|
/** @type {import('./MediaProgress')[]?} - Only included when extended */
|
||||||
this.mediaProgresses
|
this.mediaProgresses
|
||||||
|
|
||||||
// Temporary accessToken, not stored in database
|
|
||||||
/** @type {string} */
|
|
||||||
this.accessToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Excludes "root" since their can only be 1 root user
|
// Excludes "root" since their can only be 1 root user
|
||||||
|
@ -526,7 +522,6 @@ 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,
|
||||||
accessToken: this.accessToken || null,
|
|
||||||
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 })) || [],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue