mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-20 10:45:32 +02:00
oauth2: Force HTTPS, check state
Also improve error handling
This commit is contained in:
parent
737d8f19b3
commit
6c88337180
1 changed files with 63 additions and 19 deletions
|
@ -97,7 +97,10 @@ export default {
|
||||||
error: null,
|
error: null,
|
||||||
showForm: false,
|
showForm: false,
|
||||||
showAddCustomHeaders: false,
|
showAddCustomHeaders: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
oauth: {
|
||||||
|
state: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -138,11 +141,28 @@ export default {
|
||||||
// audiobookshelf://oauth?code...
|
// audiobookshelf://oauth?code...
|
||||||
// urlObj.hostname for iOS and urlObj.pathname for android
|
// urlObj.hostname for iOS and urlObj.pathname for android
|
||||||
if (url.startsWith('audiobookshelf://oauth')) {
|
if (url.startsWith('audiobookshelf://oauth')) {
|
||||||
|
// Extract possible errors thrown by the SSO provider
|
||||||
|
const authError = urlObj.searchParams.get('error')
|
||||||
|
if (authError) {
|
||||||
|
console.warn(`[SSO] Received the following error: ${authError}`)
|
||||||
|
this.$toast.error(`SSO: Received the following error: ${authError}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Extract oauth2 code to be exchanged for a token
|
// Extract oauth2 code to be exchanged for a token
|
||||||
const authCode = urlObj.searchParams.get('code')
|
const authCode = urlObj.searchParams.get('code')
|
||||||
// Extract the state variable
|
// Extract the state variable
|
||||||
const state = urlObj.searchParams.get('state')
|
const state = urlObj.searchParams.get('state')
|
||||||
|
|
||||||
|
if (this.oauth.state !== state) {
|
||||||
|
console.warn(`[SSO] Wrong state returned by SSO Provider`)
|
||||||
|
this.$toast.error(`SSO: The response from the SSO Provider was invalid (wrong state)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the state variable from the component config
|
||||||
|
this.oauth.state = null
|
||||||
|
|
||||||
if (authCode) {
|
if (authCode) {
|
||||||
await this.oauthExchangeCodeForToken(authCode, state)
|
await this.oauthExchangeCodeForToken(authCode, state)
|
||||||
}
|
}
|
||||||
|
@ -151,8 +171,19 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async clickLoginWithOpenId() {
|
async clickLoginWithOpenId() {
|
||||||
|
// oauth standard requires https explicitly
|
||||||
|
if (!this.serverConfig.address.startsWith('https')) {
|
||||||
|
console.warn(`[SSO] Oauth2 requires HTTPS`)
|
||||||
|
this.$toast.error(`SSO: The URL to the server must be https:// secured`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// First request that we want to do oauth/openid and get the URL which a browser window should open
|
// First request that we want to do oauth/openid and get the URL which a browser window should open
|
||||||
const redirectUrl = await this.oauthRequest(this.serverConfig.address)
|
const redirectUrl = await this.oauthRequest(this.serverConfig.address)
|
||||||
|
if (!redirectUrl) {
|
||||||
|
// error message handled by oauthRequest
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Actually we should be able to use the redirectUrl directly for Browser.open below
|
// Actually we should be able to use the redirectUrl directly for Browser.open below
|
||||||
// However it seems that when directly using it there is a malformation and leads to the error
|
// However it seems that when directly using it there is a malformation and leads to the error
|
||||||
|
@ -172,7 +203,16 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = `${redirectUrl.protocol}//${redirectUrl.host}`
|
if (redirectUrl.protocol !== 'https:') {
|
||||||
|
console.warn(`[SSO] Insecure Redirection by SSO provider: ${redirectUrl.protocol} is not allowed. Use HTTPS`)
|
||||||
|
this.$toast.error(`SSO: The SSO provider must return a HTTPS secured URL`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to verify if the state is the same later
|
||||||
|
this.oauth.state = state
|
||||||
|
|
||||||
|
const host = `https://${redirectUrl.host}`
|
||||||
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}`
|
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}`
|
||||||
|
|
||||||
// example url for authentik
|
// example url for authentik
|
||||||
|
@ -199,20 +239,22 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Every kind of redirection is allowed [RFC6749 - 1.7]
|
||||||
|
if (!(response.status >= 300 && response.status < 400)) {
|
||||||
|
throw new Error(`Unexpected response from server: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
// Depending on iOS or Android, it can be location or Location...
|
// Depending on iOS or Android, it can be location or Location...
|
||||||
const locationHeader = response.headers[Object.keys(response.headers).find((key) => key.toLowerCase() === 'location')]
|
const locationHeader = response.headers[Object.keys(response.headers).find((key) => key.toLowerCase() === 'location')]
|
||||||
if (locationHeader) {
|
if (!locationHeader) {
|
||||||
const url = new URL(locationHeader)
|
throw new Error(`No location header in SSO answer`)
|
||||||
return url
|
|
||||||
} else {
|
|
||||||
console.log('[SSO] No location header in oauthRequest')
|
|
||||||
this.$toast.error(`SSO: Invalid answer`)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = new URL(locationHeader)
|
||||||
|
return url
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[SSO] Error in oauthRequest: ' + error)
|
console.error(`[SSO] ${error.message}`)
|
||||||
this.$toast.error(`SSO error: ${error}`)
|
this.$toast.error(`SSO Error: ${error.message}`)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async oauthExchangeCodeForToken(code, state) {
|
async oauthExchangeCodeForToken(code, state) {
|
||||||
|
@ -224,26 +266,28 @@ export default {
|
||||||
if (this.$platform === 'ios' || this.$platform === 'web') {
|
if (this.$platform === 'ios' || this.$platform === 'web') {
|
||||||
await Browser.close()
|
await Browser.close()
|
||||||
}
|
}
|
||||||
|
} catch(error) {} // No Error handling needed
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await CapacitorHttp.get({
|
const response = await CapacitorHttp.get({
|
||||||
url: backendEndpoint
|
url: backendEndpoint
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!response.data || !response.data.user || !response.data.user.token) {
|
||||||
|
throw new Error('Token data is missing in the response.')
|
||||||
|
}
|
||||||
|
|
||||||
this.serverConfig.token = response.data.user.token
|
this.serverConfig.token = response.data.user.token
|
||||||
const payload = await this.authenticateToken()
|
const payload = await this.authenticateToken()
|
||||||
|
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
console.log('[SSO] Failed getting token: ' + this.error)
|
throw new Error('Authentication failed with the provided token.')
|
||||||
this.$toast.error(`SSO error: ${this.error}`)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setUserAndConnection(payload)
|
this.setUserAndConnection(payload)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[SSO] Error in exchangeCodeForToken: ' + error)
|
console.error('[SSO] Error in exchangeCodeForToken: ', error)
|
||||||
this.$toast.error(`SSO error: ${error}`)
|
this.$toast.error(`SSO error: ${error.message || error}`)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addCustomHeaders() {
|
addCustomHeaders() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue