feat: handle oauth scope verification and errors
This commit is contained in:
@@ -15,6 +15,6 @@ module GoogleConcern
|
||||
private
|
||||
|
||||
def scope
|
||||
'email profile https://mail.google.com/'
|
||||
'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://mail.google.com/'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,16 @@ class Google::CallbacksController < OauthCallbackController
|
||||
|
||||
private
|
||||
|
||||
def verify_scopes
|
||||
granted_scopes = parsed_body['scope']&.split || []
|
||||
required_scopes = scope.split
|
||||
|
||||
missing_scopes = required_scopes - granted_scopes
|
||||
return if missing_scopes.empty?
|
||||
|
||||
raise CustomExceptions::OAuth::InsufficientScopes.new({ missing_scopes: missing_scopes })
|
||||
end
|
||||
|
||||
def provider_name
|
||||
'google'
|
||||
end
|
||||
@@ -24,4 +34,14 @@ class Google::CallbacksController < OauthCallbackController
|
||||
# from GoogleConcern
|
||||
google_client
|
||||
end
|
||||
|
||||
def handle_error(exception)
|
||||
ChatwootExceptionTracker.new(exception).capture_exception
|
||||
|
||||
error_code = exception.respond_to?(:code) ? exception.code : 'OAUTH_ERR'
|
||||
error_message = exception.message || 'OAuth authorization failed'
|
||||
|
||||
redirect_url = "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/app/accounts/#{account.id}/settings/inboxes/new/email"
|
||||
redirect_to "#{redirect_url}?error=#{CGI.escape(error_message)}&code=#{error_code}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,10 +5,10 @@ class OauthCallbackController < ApplicationController
|
||||
redirect_uri: "#{base_url}/#{provider_name}/callback"
|
||||
)
|
||||
|
||||
verify_scopes
|
||||
handle_response
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
handle_error(e)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -63,6 +63,19 @@ class OauthCallbackController < ApplicationController
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def verify_scopes
|
||||
true
|
||||
end
|
||||
|
||||
def failure_redirect_url
|
||||
'/'
|
||||
end
|
||||
|
||||
def handle_error(exception)
|
||||
ChatwootExceptionTracker.new(exception).capture_exception
|
||||
redirect_to failure_redirect_url
|
||||
end
|
||||
|
||||
def create_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import ForwardToOption from './emailChannels/ForwardToOption.vue';
|
||||
import Microsoft from './emailChannels/Microsoft.vue';
|
||||
import Google from './emailChannels/Google.vue';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const provider = ref('');
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const globalConfig = getters['globalConfig/get'];
|
||||
@@ -45,6 +49,15 @@ const emailProviderList = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const checkForOAuthError = () => {
|
||||
const { error } = route.query;
|
||||
if (error) {
|
||||
useAlert(decodeURIComponent(error));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(checkForOAuthError);
|
||||
|
||||
function onClick(emailProvider) {
|
||||
if (emailProvider.isEnabled) {
|
||||
provider.value = emailProvider.key;
|
||||
|
||||
@@ -48,6 +48,11 @@ en:
|
||||
email_already_exists: 'You have already signed up for an account with %{email}'
|
||||
invalid_params: 'Invalid, please check the signup paramters and try again'
|
||||
failed: Signup failed
|
||||
oauth:
|
||||
insufficient_scopes: 'Insufficient permissions granted. Missing scopes: %{scopes}'
|
||||
authorization_failed: 'Authorization failed for %{provider}'
|
||||
token_exchange_failed: 'Failed to exchange authorization code for %{provider}'
|
||||
invalid_state: 'Invalid or missing state parameter'
|
||||
data_import:
|
||||
data_type:
|
||||
invalid: Invalid data type
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module CustomExceptions::OAuth
|
||||
class InsufficientScopes < CustomExceptions::Base
|
||||
def message
|
||||
I18n.t('errors.oauth.insufficient_scopes', scopes: @data[:missing_scopes].join(', '))
|
||||
end
|
||||
|
||||
def code
|
||||
'SCOPE_ERR'
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
message: message,
|
||||
code: code,
|
||||
missing_scopes: @data[:missing_scopes]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class AuthorizationFailed < CustomExceptions::Base
|
||||
def message
|
||||
I18n.t('errors.oauth.authorization_failed', provider: @data[:provider])
|
||||
end
|
||||
|
||||
def code
|
||||
'AUTH_ERR'
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
message: message,
|
||||
code: code,
|
||||
provider: @data[:provider]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class TokenExchangeFailed < CustomExceptions::Base
|
||||
def message
|
||||
I18n.t('errors.oauth.token_exchange_failed', provider: @data[:provider])
|
||||
end
|
||||
|
||||
def code
|
||||
'TOKEN_ERR'
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
message: message,
|
||||
code: code,
|
||||
provider: @data[:provider]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidState < CustomExceptions::Base
|
||||
def message
|
||||
I18n.t('errors.oauth.invalid_state')
|
||||
end
|
||||
|
||||
def code
|
||||
'STATE_ERR'
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
message: message,
|
||||
code: code
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user