Skip to content

Commit

Permalink
Improve social authentication (#5349)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marishka17 committed Dec 10, 2022
1 parent 980c019 commit a3b4f97
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 54 deletions.
11 changes: 11 additions & 0 deletions cvat-core/src/api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ export default function implementAPI(cvat) {
return result;
};

cvat.server.loginWithSocialAccount.implementation = async (
provider: string,
code: string,
authParams?: string,
process?: string,
scope?: string,
) => {
const result = await serverProxy.server.loginWithSocialAccount(provider, code, authParams, process, scope);
return result;
};

cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
Expand Down
24 changes: 12 additions & 12 deletions cvat-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,6 @@ function build() {
const result = await PluginRegistry.apiWrapper(cvat.server.advancedAuthentication);
return result;
},
/**
* Method returns enabled advanced authentication methods
* @method advancedAuthentication
* @async
* @memberof module:API.cvat.server
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async advancedAuthentication() {
const result = await PluginRegistry.apiWrapper(cvat.server.advancedAuthentication);
return result;
},
/**
* Method allows to change user password
* @method changePassword
Expand Down Expand Up @@ -306,6 +294,18 @@ function build() {
const result = await PluginRegistry.apiWrapper(cvat.server.installedApps);
return result;
},
async loginWithSocialAccount(
provider: string,
code: string,
authParams?: string,
process?: string,
scope?: string,
) {
const result = await PluginRegistry.apiWrapper(
cvat.server.loginWithSocialAccount, provider, code, authParams, process, scope,
);
return result;
},
},
/**
* Namespace is used for getting projects
Expand Down
36 changes: 31 additions & 5 deletions cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,35 @@ async function login(credential, password) {
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}

async function loginWithSocialAccount(
provider: string,
code: string,
authParams?: string,
process?: string,
scope?: string,
) {
removeToken();
const data = {
code,
...(process ? { process } : {}),
...(scope ? { scope } : {}),
...(authParams ? { auth_params: authParams } : {}),
};
let authenticationResponse = null;
try {
authenticationResponse = await Axios.post(`${config.backendAPI}/auth/${provider}/login/token`, data,
{
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}

token = authenticationResponse.data.key;
store.set('token', token);
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}

async function logout() {
try {
await Axios.post(`${config.backendAPI}/auth/logout`, {
Expand Down Expand Up @@ -447,11 +476,7 @@ async function getSelf() {

async function authorized() {
try {
const response = await getSelf();
if (!store.get('token')) {
store.set('token', response.key);
Axios.defaults.headers.common.Authorization = `Token ${response.key}`;
}
await getSelf();
} catch (serverError) {
if (serverError.code === 401) {
// In CVAT app we use two types of authentication,
Expand Down Expand Up @@ -2255,6 +2280,7 @@ export default Object.freeze({
request: serverRequest,
userAgreements,
installedApps,
loginWithSocialAccount,
}),

projects: Object.freeze({
Expand Down
6 changes: 6 additions & 0 deletions cvat-ui/src/components/cvat-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'antd/dist/antd.css';
import LogoutComponent from 'components/logout-component';
import LoginPageContainer from 'containers/login-page/login-page';
import LoginWithTokenComponent from 'components/login-with-token/login-with-token';
import LoginWithSocialAppComponent from 'components/login-with-social-app/login-with-social-app';
import RegisterPageContainer from 'containers/register-page/register-page';
import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page';
import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
Expand Down Expand Up @@ -502,6 +503,11 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
path='/auth/login-with-token/:token'
component={LoginWithTokenComponent}
/>
<Route
exact
path='/auth/login-with-social-app/'
component={LoginWithSocialAppComponent}
/>
<Route exact path='/auth/password/reset' component={ResetPasswordPageComponent} />
<Route
exact
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React, { useEffect } from 'react';
import { useLocation, useHistory } from 'react-router';
import notification from 'antd/lib/notification';
import Spin from 'antd/lib/spin';

import { getCore } from 'cvat-core-wrapper';

const cvat = getCore();

export default function LoginWithSocialAppComponent(): JSX.Element {
const location = useLocation();
const history = useHistory();
const search = new URLSearchParams(location.search);

useEffect(() => {
const provider = search.get('provider');
const code = search.get('code');
const process = search.get('process');
const scope = search.get('scope');
const authParams = search.get('auth_params');

if (provider && code) {
cvat.server.loginWithSocialAccount(provider, code, authParams, process, scope)
.then(() => window.location.reload())
.catch((exception: Error) => {
if (exception.message.includes('Unverified email')) {
history.push('/auth/email-verification-sent');
}
history.push('/auth/login');
notification.error({
message: 'Could not log in with social account',
description: 'Go to developer console',
});
return Promise.reject(exception);
});
}
}, []);

return (
<div className='cvat-login-page cvat-spinner-container'>
<Spin size='large' className='cvat-spinner' />
</div>
);
}
7 changes: 0 additions & 7 deletions cvat/apps/engine/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,6 @@ class MetaUserSerializerExtension(AnyOfProxySerializerExtension):
# field here, because these serializers don't have such.
target_component = 'MetaUser'

class MetaSelfUserSerializerExtension(AnyOfProxySerializerExtension):
# Need to replace oneOf to anyOf for MetaUser variants
# Otherwise, clients cannot distinguish between classes
# using just input data. Also, we can't use discrimintator
# field here, because these serializers don't have such.
target_component = 'MetaSelfUser'

class PolymorphicProjectSerializerExtension(AnyOfProxySerializerExtension):
# Need to replace oneOf to anyOf for PolymorphicProject variants
# Otherwise, clients cannot distinguish between classes
Expand Down
6 changes: 0 additions & 6 deletions cvat/apps/engine/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,6 @@ class Meta:
'last_login': { 'allow_null': True }
}

class SelfUserSerializer(UserSerializer):
key = serializers.CharField(allow_blank=True, required=False)

class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + ('key',)

class AttributeSerializer(serializers.ModelSerializer):
values = serializers.ListField(allow_empty=True,
child=serializers.CharField(max_length=200),
Expand Down
16 changes: 5 additions & 11 deletions cvat/apps/engine/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseBadRequest
from django.utils import timezone

from dj_rest_auth.models import get_token_model
from dj_rest_auth.app_settings import create_token

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer,
Expand Down Expand Up @@ -58,7 +55,7 @@
)
from cvat.apps.engine.models import CloudStorage as CloudStorageModel
from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, SelfUserSerializer,
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaReadSerializer, DataMetaWriteSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobReadSerializer, JobWriteSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectReadSerializer, ProjectWriteSerializer, ProjectSearchSerializer,
Expand Down Expand Up @@ -1938,28 +1935,25 @@ def get_serializer_class(self):
is_self = int(self.kwargs.get("pk", 0)) == user.id or \
self.action == "self"
if user.is_staff:
return UserSerializer if not is_self else SelfUserSerializer
return UserSerializer if not is_self else UserSerializer
else:
if is_self and self.request.method in SAFE_METHODS:
return SelfUserSerializer
return UserSerializer
else:
return BasicUserSerializer

@extend_schema(summary='Method returns an instance of a user who is currently authorized',
responses={
'200': PolymorphicProxySerializer(component_name='MetaSelfUser',
'200': PolymorphicProxySerializer(component_name='MetaUser',
serializers=[
SelfUserSerializer, BasicUserSerializer,
UserSerializer, BasicUserSerializer,
], resource_type_field_name=None),
})
@action(detail=False, methods=['GET'])
def self(self, request):
"""
Method returns an instance of a user who is currently authorized
"""
token_model = get_token_model()
token = create_token(token_model, request.user, None)
request.user.key = token
serializer_class = self.get_serializer_class()
serializer = serializer_class(request.user, context={ "request": request })
return Response(serializer.data)
Expand Down
21 changes: 20 additions & 1 deletion cvat/apps/iam/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# SPDX-License-Identifier: MIT

from dj_rest_auth.registration.serializers import RegisterSerializer
from dj_rest_auth.registration.serializers import RegisterSerializer, SocialLoginSerializer
from dj_rest_auth.serializers import PasswordResetSerializer, LoginSerializer
from rest_framework.exceptions import ValidationError
from rest_framework import serializers
Expand Down Expand Up @@ -79,3 +79,22 @@ def is_username_authentication():
raise ValidationError('Unable to login with provided credentials')

return self._validate_username_email(username, email, password)


class SocialLoginSerializerEx(SocialLoginSerializer):
auth_params = serializers.CharField(required=False, allow_blank=True, default='')
process = serializers.CharField(required=False, allow_blank=True, default='login')
scope = serializers.CharField(required=False, allow_blank=True, default='')

def get_social_login(self, adapter, app, token, response):
request = self._get_request()
social_login = adapter.complete_login(request, app, token, response=response)
social_login.token = token

social_login.state = {
'process': self.initial_data.get('process'),
'scope': self.initial_data.get('scope'),
'auth_params': self.initial_data.get('auth_params'),
}

return social_login
4 changes: 3 additions & 1 deletion cvat/apps/iam/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
github_oauth2_callback as github_callback,
google_oauth2_login as google_login,
google_oauth2_callback as google_callback,
LoginViewEx,
LoginViewEx, GitHubLogin, GoogleLogin,
)

urlpatterns = [
Expand Down Expand Up @@ -52,8 +52,10 @@
urlpatterns += [
path('github/login/', github_login, name='github_login'),
path('github/login/callback/', github_callback, name='github_callback'),
path('github/login/token', GitHubLogin.as_view()),
path('google/login/', google_login, name='google_login'),
path('google/login/callback/', google_callback, name='google_callback'),
path('google/login/token', GoogleLogin.as_view()),
]

urlpatterns = [path('auth/', include(urlpatterns))]

0 comments on commit a3b4f97

Please sign in to comment.