Skip to content

Commit

Permalink
feat: Authenticate by SSO tokens (#1439)
Browse files Browse the repository at this point in the history
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
  • Loading branch information
sojan-official and pranavrajs committed Nov 25, 2020
1 parent cb2a528 commit a988724
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 8 deletions.
3 changes: 2 additions & 1 deletion app/builders/account_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def email_to_name(email)
end

def create_user
password = Time.now.to_i
password = SecureRandom.alphanumeric(12)

@user = User.new(email: @email,
password: password,
password_confirmation: password,
Expand Down
30 changes: 30 additions & 0 deletions app/controllers/devise_overrides/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,38 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
# Prevent session parameter from being passed
# Unpermitted parameter: session
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
render partial: 'devise/auth.json', locals: { resource: @resource }
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
25 changes: 20 additions & 5 deletions app/javascript/dashboard/routes/login/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</h2>
</div>
<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()">
<div class="column log-in-form">
<label :class="{ error: $v.credentials.email.$error }">
Expand Down Expand Up @@ -47,7 +47,6 @@
button-class="large expanded"
>
</woot-submit-button>
<!-- <input type="submit" class="button " v-on:click.prevent="login()" v-bind:value="" > -->
</div>
</form>
<div class="column text-center sigin__footer">
Expand All @@ -63,13 +62,12 @@
</p>
</div>
</div>
<woot-spinner v-else size="" />
</div>
</div>
</template>

<script>
/* global bus */
import { required, email } from 'vuelidate/lib/validators';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
Expand All @@ -80,6 +78,12 @@ export default {
WootSubmitButton,
},
mixins: [globalConfigMixin],
props: {
ssoAuthToken: { type: String, default: '' },
redirectUrl: { type: String, default: '' },
config: { type: String, default: '' },
email: { type: String, default: '' },
},
data() {
return {
// We need to initialize the component with any
Expand Down Expand Up @@ -111,6 +115,11 @@ export default {
globalConfig: 'globalConfig/get',
}),
},
created() {
if (this.ssoAuthToken) {
this.login();
}
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
Expand All @@ -124,15 +133,21 @@ export default {
login() {
this.loginApi.showLoading = true;
const credentials = {
email: this.credentials.email,
email: this.email ? this.email : this.credentials.email,
password: this.credentials.password,
sso_auth_token: this.ssoAuthToken,
};
this.$store
.dispatch('login', credentials)
.then(() => {
this.showAlert(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
.catch(response => {
// Reset URL Params if the authenication is invalid
if (this.email) {
window.location = '/app/login';
}
if (response && response.status === 401) {
this.showAlert(this.$t('LOGIN.API.UNAUTH'));
return;
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/dashboard/routes/login/login.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export default {
path: frontendURL('login'),
name: '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 changes: 23 additions & 0 deletions app/models/concerns/sso_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions app/models/conversation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def unmute!
end

def muted?
!Redis::Alfred.get(mute_key).nil?
Redis::Alfred.get(mute_key).present?
end

def lock!
Expand Down Expand Up @@ -287,7 +287,7 @@ def create_unmuted_message
end

def mute_key
format('CONVERSATION::%<id>d::MUTED', id: id)
format(Redis::RedisKeys::CONVERSATION_MUTE_KEY, id: id)
end

def mute_period
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class User < ApplicationRecord
include Pubsubable
include Rails.application.routes.url_helpers
include Reportable
include SsoAuthenticatable

devise :database_authenticatable,
:registerable,
Expand Down
11 changes: 11 additions & 0 deletions lib/redis/redis_keys.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
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

## Conversation keys
# Detect whether to send an email reply to the conversation
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
# hash containing user_id key and status as value
Expand All @@ -12,6 +22,7 @@ module Redis::RedisKeys
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%<account_id>d::USERS'.freeze

## 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
REAUTHORIZATION_REQUIRED = 'REAUTHORIZATION_REQUIRED:%<obj_type>s:%<obj_id>d'.freeze
end
31 changes: 31 additions & 0 deletions spec/controllers/devise/session_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,36 @@
expect(response.body).to include(user.email)
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
21 changes: 21 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,25 @@
it { expect(user.pubsub_token).not_to eq(nil) }
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
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

0 comments on commit a988724

Please sign in to comment.