Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Google OAuth for login & signup (#6346)
This PR adds Google OAuth for all existing users, allowing users to log in or sign up via their Google account. --------- Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Sojan <sojan@pepalo.com>
- Loading branch information
1 parent
2c8ecbe
commit 7be2ef3
Showing
26 changed files
with
567 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,3 +62,7 @@ test/cypress/videos/* | |
/config/*.enc | ||
|
||
.vscode/settings.json | ||
|
||
# yalc for local testing | ||
.yalc | ||
yalc.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
app/controllers/devise_overrides/omniauth_callbacks_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController | ||
include EmailHelper | ||
|
||
def omniauth_success | ||
get_resource_from_auth_hash | ||
|
||
@resource.present? ? sign_in_user : sign_up_user | ||
end | ||
|
||
private | ||
|
||
def sign_in_user | ||
@resource.skip_confirmation! if confirmable_enabled? | ||
|
||
# once the resource is found and verified | ||
# we can just send them to the login page again with the SSO params | ||
# that will log them in | ||
encoded_email = ERB::Util.url_encode(@resource.email) | ||
redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token) | ||
end | ||
|
||
def sign_up_user | ||
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed? | ||
return redirect_to login_page_url(error: 'business-account-only') unless validate_business_account? | ||
|
||
create_account_for_user | ||
token = @resource.send(:set_reset_password_token) | ||
frontend_url = ENV.fetch('FRONTEND_URL', nil) | ||
redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}" | ||
end | ||
|
||
def login_page_url(error: nil, email: nil, sso_auth_token: nil) | ||
frontend_url = ENV.fetch('FRONTEND_URL', nil) | ||
params = { email: email, sso_auth_token: sso_auth_token }.compact | ||
params[:error] = error if error.present? | ||
|
||
"#{frontend_url}/app/login?#{params.to_query}" | ||
end | ||
|
||
def account_signup_allowed? | ||
# set it to true by default, this is the behaviour across the app | ||
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') != 'false' | ||
end | ||
|
||
def resource_class(_mapping = nil) | ||
User | ||
end | ||
|
||
def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName | ||
# find the user with their email instead of UID and token | ||
@resource = resource_class.where( | ||
email: auth_hash['info']['email'] | ||
).first | ||
end | ||
|
||
def validate_business_account? | ||
# return true if the user is a business account, false if it is a gmail account | ||
auth_hash['info']['email'].exclude?('@gmail.com') | ||
end | ||
|
||
def create_account_for_user | ||
@resource, @account = AccountBuilder.new( | ||
account_name: extract_domain_without_tld(auth_hash['info']['email']), | ||
user_full_name: auth_hash['info']['name'], | ||
email: auth_hash['info']['email'], | ||
locale: I18n.locale, | ||
confirmed: auth_hash['info']['email_verified'] | ||
).perform | ||
Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image']) | ||
end | ||
|
||
def default_devise_mapping | ||
'user' | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module EmailHelper | ||
def extract_domain_without_tld(email) | ||
domain = email.split('@').last | ||
domain.split('.').first | ||
end | ||
end |
69 changes: 69 additions & 0 deletions
69
app/javascript/dashboard/components/ui/Auth/GoogleOAuthButton.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { shallowMount } from '@vue/test-utils'; | ||
import GoogleOAuthButton from './GoogleOAuthButton.vue'; | ||
|
||
function getWrapper(showSeparator, buttonSize) { | ||
return shallowMount(GoogleOAuthButton, { | ||
propsData: { showSeparator: showSeparator, buttonSize: buttonSize }, | ||
methods: { | ||
$t(text) { | ||
return text; | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
describe('GoogleOAuthButton.vue', () => { | ||
beforeEach(() => { | ||
window.chatwootConfig = { | ||
googleOAuthClientId: 'clientId', | ||
googleOAuthCallbackUrl: 'http://localhost:3000/test-callback', | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
window.chatwootConfig = {}; | ||
}); | ||
|
||
it('renders the OR separator if showSeparator is true', () => { | ||
const wrapper = getWrapper(true); | ||
expect(wrapper.find('.separator').exists()).toBe(true); | ||
}); | ||
|
||
it('does not render the OR separator if showSeparator is false', () => { | ||
const wrapper = getWrapper(false); | ||
expect(wrapper.find('.separator').exists()).toBe(false); | ||
}); | ||
|
||
it('generates the correct Google Auth URL', () => { | ||
const wrapper = getWrapper(); | ||
const googleAuthUrl = new URL(wrapper.vm.getGoogleAuthUrl()); | ||
|
||
const params = googleAuthUrl.searchParams; | ||
expect(googleAuthUrl.origin).toBe('https://accounts.google.com'); | ||
expect(googleAuthUrl.pathname).toBe('/o/oauth2/auth/oauthchooseaccount'); | ||
expect(params.get('client_id')).toBe('clientId'); | ||
expect(params.get('redirect_uri')).toBe( | ||
'http://localhost:3000/test-callback' | ||
); | ||
expect(params.get('response_type')).toBe('code'); | ||
expect(params.get('scope')).toBe('email profile'); | ||
}); | ||
|
||
it('responds to buttonSize prop properly', () => { | ||
let wrapper = getWrapper(true, 'tiny'); | ||
expect(wrapper.find('.button.tiny').exists()).toBe(true); | ||
|
||
wrapper = getWrapper(true, 'small'); | ||
expect(wrapper.find('.button.small').exists()).toBe(true); | ||
|
||
wrapper = getWrapper(true, 'large'); | ||
expect(wrapper.find('.button.large').exists()).toBe(true); | ||
|
||
// should not render either | ||
wrapper = getWrapper(true, 'default'); | ||
expect(wrapper.find('.button.small').exists()).toBe(false); | ||
expect(wrapper.find('.button.tiny').exists()).toBe(false); | ||
expect(wrapper.find('.button.large').exists()).toBe(false); | ||
expect(wrapper.find('.button').exists()).toBe(true); | ||
}); | ||
}); |
96 changes: 96 additions & 0 deletions
96
app/javascript/dashboard/components/ui/Auth/GoogleOAuthButton.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<template> | ||
<div> | ||
<div v-if="showSeparator" class="separator"> | ||
OR | ||
</div> | ||
<a :href="getGoogleAuthUrl()"> | ||
<button | ||
class="button expanded button__google_login" | ||
:class="{ | ||
// Explicit checking to ensure no other value is used | ||
large: buttonSize === 'large', | ||
small: buttonSize === 'small', | ||
tiny: buttonSize === 'tiny', | ||
}" | ||
> | ||
<img | ||
src="/assets/images/auth/google.svg" | ||
alt="Google Logo" | ||
class="icon" | ||
/> | ||
<slot>{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}</slot> | ||
</button> | ||
</a> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
const validButtonSizes = ['small', 'tiny', 'large']; | ||
export default { | ||
props: { | ||
showSeparator: { | ||
type: Boolean, | ||
default: true, | ||
}, | ||
buttonSize: { | ||
type: String, | ||
default: undefined, | ||
validator: value => | ||
validButtonSizes.includes(value) || value === undefined, | ||
}, | ||
}, | ||
methods: { | ||
getGoogleAuthUrl() { | ||
// Ideally a request to /auth/google_oauth2 should be made | ||
// Creating the URL manually because the devise-token-auth with | ||
// omniauth has a standing issue on redirecting the post request | ||
// https://github.com/lynndylanhurley/devise_token_auth/issues/1466 | ||
const baseUrl = | ||
'https://accounts.google.com/o/oauth2/auth/oauthchooseaccount'; | ||
const clientId = window.chatwootConfig.googleOAuthClientId; | ||
const redirectUri = window.chatwootConfig.googleOAuthCallbackUrl; | ||
const responseType = 'code'; | ||
const scope = 'email profile'; | ||
// Build the query string | ||
const queryString = new URLSearchParams({ | ||
client_id: clientId, | ||
redirect_uri: redirectUri, | ||
response_type: responseType, | ||
scope: scope, | ||
}).toString(); | ||
// Construct the full URL | ||
return `${baseUrl}?${queryString}`; | ||
}, | ||
}, | ||
}; | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
.separator { | ||
display: flex; | ||
align-items: center; | ||
margin: var(--space-two) var(--space-zero); | ||
gap: var(--space-one); | ||
color: var(--s-300); | ||
font-size: var(--font-size-small); | ||
&::before, | ||
&::after { | ||
content: ''; | ||
flex: 1; | ||
height: 1px; | ||
background: var(--s-100); | ||
} | ||
} | ||
.button__google_login { | ||
background: var(--white); | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
gap: var(--space-one); | ||
border: 1px solid var(--s-100); | ||
color: var(--b-800); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.