feat: Authenticate by SSO tokens (#1439)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
@@ -67,7 +67,8 @@ class AccountBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_user
|
def create_user
|
||||||
password = Time.now.to_i
|
password = SecureRandom.alphanumeric(12)
|
||||||
|
|
||||||
@user = User.new(email: @email,
|
@user = User.new(email: @email,
|
||||||
password: password,
|
password: password,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
|
|||||||
@@ -2,8 +2,38 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
|||||||
# Prevent session parameter from being passed
|
# Prevent session parameter from being passed
|
||||||
# Unpermitted parameter: session
|
# Unpermitted parameter: session
|
||||||
wrap_parameters format: []
|
wrap_parameters format: []
|
||||||
|
before_action :process_sso_auth_token, only: [:create]
|
||||||
|
|
||||||
|
def create
|
||||||
|
# Authenticate user via the temporary sso auth token
|
||||||
|
if params[:sso_auth_token].present? && @resource.present?
|
||||||
|
authenticate_resource_with_sso_token
|
||||||
|
yield @resource if block_given?
|
||||||
|
render_create_success
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def render_create_success
|
def render_create_success
|
||||||
render partial: 'devise/auth.json', locals: { resource: @resource }
|
render partial: 'devise/auth.json', locals: { resource: @resource }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authenticate_resource_with_sso_token
|
||||||
|
@token = @resource.create_token
|
||||||
|
@resource.save
|
||||||
|
|
||||||
|
sign_in(:user, @resource, store: false, bypass: false)
|
||||||
|
# invalidate the token after the user is signed in
|
||||||
|
@resource.invalidate_sso_auth_token(params[:sso_auth_token])
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_sso_auth_token
|
||||||
|
return if params[:email].blank?
|
||||||
|
|
||||||
|
user = User.find_by(email: params[:email])
|
||||||
|
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="row align-center">
|
<div class="row align-center">
|
||||||
<div class="small-12 medium-4 column">
|
<div v-if="!email" class="small-12 medium-4 column">
|
||||||
<form class="login-box column align-self-top" @submit.prevent="login()">
|
<form class="login-box column align-self-top" @submit.prevent="login()">
|
||||||
<div class="column log-in-form">
|
<div class="column log-in-form">
|
||||||
<label :class="{ error: $v.credentials.email.$error }">
|
<label :class="{ error: $v.credentials.email.$error }">
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
button-class="large expanded"
|
button-class="large expanded"
|
||||||
>
|
>
|
||||||
</woot-submit-button>
|
</woot-submit-button>
|
||||||
<!-- <input type="submit" class="button " v-on:click.prevent="login()" v-bind:value="" > -->
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="column text-center sigin__footer">
|
<div class="column text-center sigin__footer">
|
||||||
@@ -63,13 +62,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<woot-spinner v-else size="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global bus */
|
|
||||||
|
|
||||||
import { required, email } from 'vuelidate/lib/validators';
|
import { required, email } from 'vuelidate/lib/validators';
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
|
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
|
||||||
@@ -80,6 +78,12 @@ export default {
|
|||||||
WootSubmitButton,
|
WootSubmitButton,
|
||||||
},
|
},
|
||||||
mixins: [globalConfigMixin],
|
mixins: [globalConfigMixin],
|
||||||
|
props: {
|
||||||
|
ssoAuthToken: { type: String, default: '' },
|
||||||
|
redirectUrl: { type: String, default: '' },
|
||||||
|
config: { type: String, default: '' },
|
||||||
|
email: { type: String, default: '' },
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// We need to initialize the component with any
|
// We need to initialize the component with any
|
||||||
@@ -111,6 +115,11 @@ export default {
|
|||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
if (this.ssoAuthToken) {
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showAlert(message) {
|
showAlert(message) {
|
||||||
// Reset loading, current selected agent
|
// Reset loading, current selected agent
|
||||||
@@ -124,8 +133,9 @@ export default {
|
|||||||
login() {
|
login() {
|
||||||
this.loginApi.showLoading = true;
|
this.loginApi.showLoading = true;
|
||||||
const credentials = {
|
const credentials = {
|
||||||
email: this.credentials.email,
|
email: this.email ? this.email : this.credentials.email,
|
||||||
password: this.credentials.password,
|
password: this.credentials.password,
|
||||||
|
sso_auth_token: this.ssoAuthToken,
|
||||||
};
|
};
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('login', credentials)
|
.dispatch('login', credentials)
|
||||||
@@ -133,6 +143,11 @@ export default {
|
|||||||
this.showAlert(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
this.showAlert(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
||||||
})
|
})
|
||||||
.catch(response => {
|
.catch(response => {
|
||||||
|
// Reset URL Params if the authenication is invalid
|
||||||
|
if (this.email) {
|
||||||
|
window.location = '/app/login';
|
||||||
|
}
|
||||||
|
|
||||||
if (response && response.status === 401) {
|
if (response && response.status === 401) {
|
||||||
this.showAlert(this.$t('LOGIN.API.UNAUTH'));
|
this.showAlert(this.$t('LOGIN.API.UNAUTH'));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export default {
|
|||||||
path: frontendURL('login'),
|
path: frontendURL('login'),
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login,
|
component: Login,
|
||||||
|
props: route => ({
|
||||||
|
config: route.query.config,
|
||||||
|
email: route.query.email,
|
||||||
|
ssoAuthToken: route.query.sso_auth_token,
|
||||||
|
redirectUrl: route.query.route_url,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
23
app/models/concerns/sso_authenticatable.rb
Normal file
23
app/models/concerns/sso_authenticatable.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module SsoAuthenticatable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def generate_sso_auth_token
|
||||||
|
token = SecureRandom.hex(32)
|
||||||
|
::Redis::Alfred.setex(sso_token_key(token), true, 5.minutes)
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalidate_sso_auth_token(token)
|
||||||
|
::Redis::Alfred.delete(sso_token_key(token))
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_sso_auth_token?(token)
|
||||||
|
::Redis::Alfred.get(sso_token_key(token)).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sso_token_key(token)
|
||||||
|
format(::Redis::RedisKeys::USER_SSO_AUTH_TOKEN, user_id: id, token: token)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -98,7 +98,7 @@ class Conversation < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def muted?
|
def muted?
|
||||||
!Redis::Alfred.get(mute_key).nil?
|
Redis::Alfred.get(mute_key).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock!
|
def lock!
|
||||||
@@ -287,7 +287,7 @@ class Conversation < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mute_key
|
def mute_key
|
||||||
format('CONVERSATION::%<id>d::MUTED', id: id)
|
format(Redis::RedisKeys::CONVERSATION_MUTE_KEY, id: id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute_period
|
def mute_period
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class User < ApplicationRecord
|
|||||||
include Pubsubable
|
include Pubsubable
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
include Reportable
|
include Reportable
|
||||||
|
include SsoAuthenticatable
|
||||||
|
|
||||||
devise :database_authenticatable,
|
devise :database_authenticatable,
|
||||||
:registerable,
|
:registerable,
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
module Redis::RedisKeys
|
module Redis::RedisKeys
|
||||||
|
## Inbox Keys
|
||||||
|
# Array storing the ordered ids for agent round robin assignment
|
||||||
ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%<inbox_id>d'.freeze
|
ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%<inbox_id>d'.freeze
|
||||||
|
|
||||||
|
## Conversation keys
|
||||||
|
# Detect whether to send an email reply to the conversation
|
||||||
CONVERSATION_MAILER_KEY = 'CONVERSATION::%<conversation_id>d'.freeze
|
CONVERSATION_MAILER_KEY = 'CONVERSATION::%<conversation_id>d'.freeze
|
||||||
|
# Whether a conversation is muted ?
|
||||||
|
CONVERSATION_MUTE_KEY = 'CONVERSATION::%<id>d::MUTED'.freeze
|
||||||
|
|
||||||
|
## User Keys
|
||||||
|
# SSO Auth Tokens
|
||||||
|
USER_SSO_AUTH_TOKEN = 'USER_SSO_AUTH_TOKEN::%<user_id>d::%<token>s'.freeze
|
||||||
|
|
||||||
## Online Status Keys
|
## Online Status Keys
|
||||||
# hash containing user_id key and status as value
|
# hash containing user_id key and status as value
|
||||||
@@ -12,6 +22,7 @@ module Redis::RedisKeys
|
|||||||
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%<account_id>d::USERS'.freeze
|
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%<account_id>d::USERS'.freeze
|
||||||
|
|
||||||
## Authorization Status Keys
|
## Authorization Status Keys
|
||||||
|
# Used to track token expiry and such issues for facebook slack integrations etc
|
||||||
AUTHORIZATION_ERROR_COUNT = 'AUTHORIZATION_ERROR_COUNT:%<obj_type>s:%<obj_id>d'.freeze
|
AUTHORIZATION_ERROR_COUNT = 'AUTHORIZATION_ERROR_COUNT:%<obj_type>s:%<obj_id>d'.freeze
|
||||||
REAUTHORIZATION_REQUIRED = 'REAUTHORIZATION_REQUIRED:%<obj_type>s:%<obj_id>d'.freeze
|
REAUTHORIZATION_REQUIRED = 'REAUTHORIZATION_REQUIRED:%<obj_type>s:%<obj_id>d'.freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,5 +30,36 @@ RSpec.describe 'Session', type: :request do
|
|||||||
expect(response.body).to include(user.email)
|
expect(response.body).to include(user.email)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when it is invalid sso auth token' do
|
||||||
|
let!(:user) { create(:user, password: 'test1234', account: account) }
|
||||||
|
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
params = { email: user.email, sso_auth_token: SecureRandom.hex(32) }
|
||||||
|
|
||||||
|
post new_user_session_url,
|
||||||
|
params: params,
|
||||||
|
as: :json
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
expect(response.body).to include('Invalid login credentials')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when with valid sso auth token' do
|
||||||
|
let!(:user) { create(:user, password: 'test1234', account: account) }
|
||||||
|
|
||||||
|
it 'returns successful auth response' do
|
||||||
|
params = { email: user.email, sso_auth_token: user.generate_sso_auth_token }
|
||||||
|
|
||||||
|
post new_user_session_url, params: params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include(user.email)
|
||||||
|
|
||||||
|
# token won't work on a subsequent request
|
||||||
|
post new_user_session_url, params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,4 +26,25 @@ RSpec.describe User do
|
|||||||
it { expect(user.pubsub_token).not_to eq(nil) }
|
it { expect(user.pubsub_token).not_to eq(nil) }
|
||||||
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
|
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'sso_auth_token' do
|
||||||
|
it 'can generate multiple sso tokens which can be validated' do
|
||||||
|
sso_auth_token1 = user.generate_sso_auth_token
|
||||||
|
sso_auth_token2 = user.generate_sso_auth_token
|
||||||
|
expect(sso_auth_token1).present?
|
||||||
|
expect(sso_auth_token2).present?
|
||||||
|
expect(user.valid_sso_auth_token?(sso_auth_token1)).to eq true
|
||||||
|
expect(user.valid_sso_auth_token?(sso_auth_token2)).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'wont validate an invalid token' do
|
||||||
|
expect(user.valid_sso_auth_token?(SecureRandom.hex(32))).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'wont validate an invalidated token' do
|
||||||
|
sso_auth_token = user.generate_sso_auth_token
|
||||||
|
user.invalidate_sso_auth_token(sso_auth_token)
|
||||||
|
expect(user.valid_sso_auth_token?(sso_auth_token)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user