Skip to content

Commit

Permalink
Feature: Mobile - Login finalization: Third-party authentications.
Browse files Browse the repository at this point in the history
Co-authored-by: Florian Liebe <[email protected]>
Co-authored-by: Dominik Klein <[email protected]>
Co-authored-by: Dusan Vuckovic <[email protected]>
  • Loading branch information
3 people committed Feb 20, 2023
1 parent 821d444 commit 657e3b4
Show file tree
Hide file tree
Showing 33 changed files with 487 additions and 73 deletions.
12 changes: 7 additions & 5 deletions app/controllers/application_controller/handles_devices.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module ApplicationController::HandlesDevices

def user_device_log(user = current_user, type = 'session')
switched_from_user_id = ENV['SWITCHED_FROM_USER_ID'] || session[:switched_from_user_id]
return true if params[:controller] == 'init' # do no device logging on static initial page
return true if %w[init mobile].include?(params[:controller]) # do no device logging on static initial page
return true if switched_from_user_id
return true if current_user_on_behalf # do no device logging for the user on behalf feature
return true if !user
Expand Down Expand Up @@ -44,13 +44,15 @@ def user_device_log(user = current_user, type = 'session')

# for sessions we need the fingperprint
if type == 'session'
if !session[:user_device_updated_at] && !params[:fingerprint] && !session[:user_device_fingerprint]
fingerprint = params[:fingerprint] || request.headers['X-Browser-Fingerprint']

if !session[:user_device_updated_at] && !fingerprint && !session[:user_device_fingerprint]
raise Exceptions::UnprocessableEntity, __('Need fingerprint param!')
end

if params[:fingerprint]
UserDevice.fingerprint_validation(params[:fingerprint])
session[:user_device_fingerprint] = params[:fingerprint]
if fingerprint
UserDevice.fingerprint_validation(fingerprint)
session[:user_device_fingerprint] = fingerprint
end
end

Expand Down
17 changes: 13 additions & 4 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class SessionsController < ApplicationController
prepend_before_action -> { authentication_check && authorize! }, only: %i[switch_to_user list delete]
skip_before_action :verify_csrf_token, only: %i[show destroy create_omniauth failure_omniauth saml_destroy]
skip_before_action :user_device_log, only: %i[create_sso]
skip_before_action :user_device_log, only: %i[create_sso create_omniauth]

def show
user = authentication_check_only
Expand Down Expand Up @@ -84,11 +84,14 @@ def create_omniauth

auth = request.env['omniauth.auth']

redirect_url = request.env['omniauth.origin']&.include?('/mobile') ? '/mobile' : '/#'

if !auth
logger.info('AUTH IS NULL, SERVICE NOT LINKED TO ACCOUNT')

# redirect to app
redirect_to '/'
redirect_to redirect_url
return
end

# Create a new user or add an auth to existing user, depending on
Expand All @@ -99,7 +102,7 @@ def create_omniauth
end

if in_maintenance_mode?(authorization.user)
redirect_to '/#'
redirect_to redirect_url
return
end

Expand All @@ -112,8 +115,14 @@ def create_omniauth
# remember last login date
authorization.user.update_last_login

# Set needed fingerprint parameter.
if request.env['omniauth.params']['fingerprint'].present?
params[:fingerprint] = request.env['omniauth.params']['fingerprint']
user_device_log(authorization.user, 'session')
end

# redirect to app
redirect_to '/'
redirect_to redirect_url
end

def failure_omniauth
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/apps/mobile/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const transition = VITE_TEST_MODE
</template>
<div
v-if="application.loaded"
class="h-full min-w-full bg-black font-sans text-sm text-white antialiased"
class="min-w-full h-full bg-black font-sans text-sm text-white antialiased"
>
<RouterView />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe('testing login maintenance mode', () => {
logout: {
success: true,
errors: null,
externalLogoutUrl: null,
},
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import useFingerprint from '@shared/composables/useFingerprint'
import { getCSRFToken } from '@shared/server/apollo/utils/csrfToken'
import type { ThirdPartyAuthProvider } from '@shared/types/authentication'

export interface Props {
providers: ThirdPartyAuthProvider[]
}

const props = defineProps<Props>()

const csrfToken = getCSRFToken()

const { fingerprint } = useFingerprint()
</script>

<template>
<section class="mt-4 mb-16 w-full max-w-md">
<p class="p-3 text-center">
{{
$c.user_show_password_login
? $t('Or sign in using')
: $t('Sign in using')
}}
</p>
<div class="flex flex-wrap gap-2">
<form
v-for="provider of props.providers"
:key="provider.name"
class="min-w-1/2-2 grow"
method="post"
:action="`${provider.url}?fingerprint=${fingerprint}`"
>
<input type="hidden" name="authenticity_token" :value="csrfToken" />
<button
class="flex h-14 w-full cursor-pointer select-none items-center justify-center rounded-xl bg-gray-600 py-2 px-4 text-white"
>
<CommonIcon
:name="`mobile-${provider.icon}`"
size="base"
decorative
class="shrink-0 ltr:mr-2.5 rtl:ml-2.5"
/>
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-xl leading-7"
>
{{ $t(provider.name) }}
</span>
</button>
</form>
</div>
</section>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/

import { renderComponent } from '@tests/support/components'
import LoginThirdParty from '../LoginThirdParty.vue'

const renderLoginThirdParty = () => {
return renderComponent(LoginThirdParty, {
props: {
providers: [
{
name: 'GitHub',
enabled: true,
icon: 'github',
url: '/auth/github',
},
{
name: 'GitLab',
enabled: true,
icon: 'gitlab',
url: '/auth/gitlab',
},
{
name: 'SAML',
enabled: true,
icon: 'saml',
url: '/auth/saml',
},
],
},
})
}

describe('LoginThirdParty.vue', () => {
it('shows the third-party login buttons', () => {
const view = renderLoginThirdParty()

expect(view.getByText('SAML')).toBeInTheDocument()
expect(view.getByText('GitHub')).toBeInTheDocument()
expect(view.getByText('GitLab')).toBeInTheDocument()

expect(view.getByIconName('mobile-saml')).toBeInTheDocument()
expect(view.getByIconName('mobile-github')).toBeInTheDocument()
expect(view.getByIconName('mobile-gitlab')).toBeInTheDocument()
})
})
2 changes: 2 additions & 0 deletions app/frontend/apps/mobile/pages/login/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const route: RouteRecordRaw[] = [
clearAllNotifications()
await authentication.logout()

if (authentication.externalLogout) return false

return '/login'
},
},
Expand Down
14 changes: 8 additions & 6 deletions app/frontend/apps/mobile/pages/login/views/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import { EnumPublicLinksScreen } from '@shared/graphql/types'
import { computed } from 'vue'
import { QueryHandler } from '@shared/server/apollo/handler'
import { PublicLinkUpdatesDocument } from '@shared/entities/public-links/graphql/subscriptions/currentLinks.api'
import { useThirdPartyAuthentication } from '@shared/composables/useThirdPartyAuthentication'
import LoginThirdParty from '../components/LoginThirdParty.vue'

const route = useRoute()
const router = useRouter()

// Output a hint when the session is no longer valid.
// This could happen because the session was deleted on the server.
Expand All @@ -35,13 +38,11 @@ if (route.query.invalidatedSession === '1') {
type: NotificationTypes.Warn,
})

// TODO: After showing this we should remove the query parameter from the URL.
router.replace({ name: 'Login' })
}

const authentication = useAuthenticationStore()

const router = useRouter()

const application = useApplicationStore()

const loginSchema = defineFormSchema([
Expand Down Expand Up @@ -166,10 +167,10 @@ const login = (formData: FormData<LoginFormData>) => {
})
}

const { enabledProviders, hasEnabledProviders } = useThirdPartyAuthentication()

const showPasswordLogin = computed(
() =>
application.config.user_show_password_login ||
!application.hasAuthProviders,
() => application.config.user_show_password_login || !hasEnabledProviders,
)
</script>

Expand Down Expand Up @@ -236,6 +237,7 @@ const showPasswordLogin = computed(
</div>
</div>
</main>
<LoginThirdParty v-if="hasEnabledProviders" :providers="enabledProviders" />
<section v-if="!showPasswordLogin" class="mb-6 w-full max-w-md text-center">
<p>
{{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const useAuthenticationChanges = () => {
router.replace('/')
}
})
} else if (!state.authenticated && session.id) {
} else if (!state.authenticated && session.id && !state.externalLogout) {
await authentication.clearAuthentication()
router.replace('/login')
}
Expand Down
88 changes: 88 additions & 0 deletions app/frontend/shared/composables/useThirdPartyAuthentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/

import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { i18n } from '@shared/i18n'
import { useApplicationStore } from '@shared/stores/application'
import type { ThirdPartyAuthProvider } from '@shared/types/authentication'

export const useThirdPartyAuthentication = () => {
const application = useApplicationStore()
const { config } = storeToRefs(application)

const providers = computed<ThirdPartyAuthProvider[]>(() => {
return [
{
name: i18n.t('Facebook'),
enabled: !!config.value.auth_facebook,
icon: 'facebook',
url: '/auth/facebook',
},
{
name: i18n.t('Twitter'),
enabled: !!config.value.auth_twitter,
icon: 'twitter',
url: '/auth/twitter',
},
{
name: i18n.t('LinkedIn'),
enabled: !!config.value.auth_linkedin,
icon: 'linkedin',
url: '/auth/linkedin',
},
{
name: i18n.t('GitHub'),
enabled: !!config.value.auth_github,
icon: 'github',
url: '/auth/github',
},
{
name: i18n.t('GitLab'),
enabled: !!config.value.auth_gitlab,
icon: 'gitlab',
url: '/auth/gitlab',
},
{
name: i18n.t('Microsoft'),
enabled: !!config.value.auth_microsoft_office365,
icon: 'microsoft',
url: '/auth/microsoft_office365',
},
{
name: i18n.t('Google'),
enabled: !!config.value.auth_google_oauth2,
icon: 'google',
url: '/auth/google_oauth2',
},
{
name: i18n.t('Weibo'),
enabled: !!config.value.auth_weibo,
icon: 'weibo',
url: '/auth/weibo',
},
{
name:
(config.value['auth_saml_credentials.display_name'] as string) ||
i18n.t('SAML'),
enabled: !!config.value.auth_saml,
icon: 'saml',
url: '/auth/saml',
},
{
name: i18n.t('SSO'),
enabled: !!config.value.auth_sso,
icon: 'sso',
url: '/auth/sso',
},
]
})

const enabledProviders = computed(() => {
return providers.value.filter((provider) => provider.enabled)
})

return {
enabledProviders: enabledProviders.value,
hasEnabledProviders: enabledProviders.value.length > 0,
}
}
1 change: 1 addition & 0 deletions app/frontend/shared/graphql/mutations/logout.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const LogoutDocument = gql`
mutation logout {
logout {
success
externalLogoutUrl
}
}
`;
Expand Down
1 change: 1 addition & 0 deletions app/frontend/shared/graphql/mutations/logout.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mutation logout {
logout {
success
externalLogoutUrl
}
}
Loading

0 comments on commit 657e3b4

Please sign in to comment.