oauth2: Force HTTPS, check state

Also improve error handling
This commit is contained in:
Denis Arnst 2023-11-03 21:29:37 +01:00
parent 737d8f19b3
commit 6c88337180

View file

@ -97,7 +97,10 @@ export default {
error: null,
showForm: false,
showAddCustomHeaders: false,
authMethods: []
authMethods: [],
oauth: {
state: null
}
}
},
computed: {
@ -138,11 +141,28 @@ export default {
// audiobookshelf://oauth?code...
// urlObj.hostname for iOS and urlObj.pathname for android
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
const authCode = urlObj.searchParams.get('code')
// Extract the state variable
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) {
await this.oauthExchangeCodeForToken(authCode, state)
}
@ -151,8 +171,19 @@ export default {
}
},
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
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
// However it seems that when directly using it there is a malformation and leads to the error
@ -172,7 +203,16 @@ export default {
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')}`
// 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...
const locationHeader = response.headers[Object.keys(response.headers).find((key) => key.toLowerCase() === 'location')]
if (locationHeader) {
if (!locationHeader) {
throw new Error(`No location header in SSO answer`)
}
const url = new URL(locationHeader)
return url
} else {
console.log('[SSO] No location header in oauthRequest')
this.$toast.error(`SSO: Invalid answer`)
return null
}
} catch (error) {
console.log('[SSO] Error in oauthRequest: ' + error)
this.$toast.error(`SSO error: ${error}`)
return null
console.error(`[SSO] ${error.message}`)
this.$toast.error(`SSO Error: ${error.message}`)
}
},
async oauthExchangeCodeForToken(code, state) {
@ -224,26 +266,28 @@ export default {
if (this.$platform === 'ios' || this.$platform === 'web') {
await Browser.close()
}
} catch(error) {} // No Error handling needed
try {
const response = await CapacitorHttp.get({
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
const payload = await this.authenticateToken()
if (!payload) {
console.log('[SSO] Failed getting token: ' + this.error)
this.$toast.error(`SSO error: ${this.error}`)
return
throw new Error('Authentication failed with the provided token.')
}
this.setUserAndConnection(payload)
} catch (error) {
console.log('[SSO] Error in exchangeCodeForToken: ' + error)
this.$toast.error(`SSO error: ${error}`)
return null
console.error('[SSO] Error in exchangeCodeForToken: ', error)
this.$toast.error(`SSO error: ${error.message || error}`)
}
},
addCustomHeaders() {