Skip to content

Commit

Permalink
Django social account sso (cvat-ai#5059)
Browse files Browse the repository at this point in the history
Issue: cvat-ai#1217

Currently there are a few proposals for SSO authentication to bypass the
current user/password login on the UI. By using Django social accounts
it is also possible to use SSO on the API, retrieving the security token
by passing the code from the OAuth2 workflow. This is an example using
Amazon Cognito, but any other social account could also be added.

### Motivation and context
Currently CVAT has no functionality to log in with SSO. Other current
proposals bypass the current Django framework to add SSO in the UI only,
but still use username and password for the API. Using Django social
accounts integrates SSO with the API as well, allowing it to be used as
an alternative to the username and password, but can also be used
together with other SSO frameworks that are UI only.

### How has this been tested?
Unit tests for SSO manager in cvat-core and integration test with
cvat-sdk for /auth/cognito endpoint.

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable by a reason then ~~explicitly
strikethrough~~ the whole
line. If you don't do that github will show an incorrect process for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [ ] I have added a description of my changes into
[CHANGELOG](https://github.com/cvat-ai/cvat/blob/develop/CHANGELOG.md)
file
- [x] I have updated the [documentation](
https://github.com/cvat-ai/cvat/blob/develop/README.md#documentation)
accordingly
- [x] I have added tests to cover my changes
- [x] I have linked related issues ([read github docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [ ] I have increased versions of npm packages if it is necessary
([cvat-canvas](https://github.com/cvat-ai/cvat/tree/develop/cvat-canvas#versioning),

[cvat-core](https://github.com/cvat-ai/cvat/tree/develop/cvat-core#versioning),
[cvat-data](https://github.com/cvat-ai/cvat/tree/develop/cvat-data#versioning)
and
[cvat-ui](https://github.com/cvat-ai/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.

Co-authored-by: Melanie Day <[email protected]>
Co-authored-by: Maria Khrustaleva <[email protected]>
Co-authored-by: Nikita Manovich <[email protected]>
  • Loading branch information
4 people authored Jan 18, 2023
1 parent 9b55a7f commit 0f0913c
Show file tree
Hide file tree
Showing 18 changed files with 245 additions and 28 deletions.
1 change: 1 addition & 0 deletions .github/workflows/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
run: |
docker load --input /tmp/cvat_server/image.tar
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
-e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \
--entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ jobs:
run: |
docker load --input /tmp/cvat_server/image.tar
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
-e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \
--entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt
Expand Down
5 changes: 5 additions & 0 deletions cvat/apps/iam/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.providers.amazon_cognito.views import AmazonCognitoOAuth2Adapter
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.exceptions import ImmediateHttpResponse
Expand Down Expand Up @@ -48,3 +49,7 @@ class GoogleAdapter(GoogleOAuth2Adapter):

def get_callback_url(self, request, app):
return settings.GOOGLE_CALLBACK_URL

class AmazonCognitoOAuth2AdapterEx(AmazonCognitoOAuth2Adapter):
def get_callback_url(self, request, app):
return settings.AMAZON_COGNITO_REDIRECT_URI
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 7 additions & 5 deletions cvat/apps/iam/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@
from allauth.account import app_settings as allauth_settings

from cvat.apps.iam.views import (
SigningView, RegisterViewEx, RulesView, ConfirmEmailViewEx,
)
from cvat.apps.iam.views import (
SigningView, CognitoLogin, RegisterViewEx, RulesView,
ConfirmEmailViewEx, LoginViewEx, GitHubLogin, GoogleLogin, SocialAuthMethods,
github_oauth2_login as github_login,
github_oauth2_callback as github_callback,
google_oauth2_login as google_login,
google_oauth2_callback as google_callback,
LoginViewEx, GitHubLogin, GoogleLogin,
SocialAuthMethods,
amazon_cognito_oauth2_login as amazon_cognito_login,
amazon_cognito_oauth2_callback as amazon_cognito_callback,
)

urlpatterns = [
Expand Down Expand Up @@ -58,6 +57,9 @@
path('google/login/', google_login, name='google_login'),
path('google/login/callback/', google_callback, name='google_callback'),
path('google/login/token', GoogleLogin.as_view()),
path('amazon-cognito/login/', amazon_cognito_login, name='amazon_cognito_login'),
path('amazon-cognito/login/callback/', amazon_cognito_callback, name='amazon_cognito_callback'),
path('amazon-cognito/login/token', CognitoLogin.as_view()),
]

urlpatterns = [path('auth/', include(urlpatterns))]
51 changes: 47 additions & 4 deletions cvat/apps/iam/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
from allauth.socialaccount.providers.oauth2.views import OAuth2CallbackView, OAuth2LoginView
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.utils import get_request_param

from furl import furl

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, OpenApiParameter, extend_schema, inline_serializer, extend_schema_view
from drf_spectacular.contrib.rest_auth import get_token_serializer_class

from cvat.apps.iam.adapters import GitHubAdapter, GoogleAdapter
from .authentication import Signer
from cvat.apps.iam.serializers import SocialLoginSerializerEx, SocialAuthMethodSerializer

Expand All @@ -49,6 +49,12 @@
else None
)

AmazonCognitoAdapter = (
import_callable(settings.SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER)
if settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
else None
)

def get_context(request):
from cvat.apps.organizations.models import Organization, Membership

Expand Down Expand Up @@ -251,8 +257,11 @@ def dispatch(self, request, *args, **kwargs):

if not code:
return HttpResponseBadRequest('Parameter code not found in request')

provider = self.adapter.provider_id.replace('_', '-')

return HttpResponseRedirect(
f'{settings.SOCIAL_APP_LOGIN_REDIRECT_URL}/?provider={self.adapter.provider_id}&code={code}'
f'{settings.SOCIAL_APP_LOGIN_REDIRECT_URL}/?provider={provider}&code={code}'
f'&auth_params={state.get("auth_params")}&process={state.get("process")}'
f'&scope={state.get("scope")}')

Expand Down Expand Up @@ -297,6 +306,17 @@ def github_oauth2_callback(*args, **kwargs):
def google_oauth2_login(*args, **kwargs):
return OAuth2LoginView.adapter_view(GoogleAdapter)(*args, **kwargs)

@extend_schema(
summary="Redirects to Amazon Cognito authentication page",
description="Redirects to the Amazon Cognito authentication page. "
"After successful authentication on the provider side, "
"a redirect to the callback endpoint is performed.",
)
@api_view(["GET"])
@permission_classes([AllowAny])
def amazon_cognito_oauth2_login(*args, **kwargs):
return OAuth2LoginView.adapter_view(AmazonCognitoAdapter)(*args, **kwargs)

@extend_schema(
summary="Checks the authentication response from Google, redirects to the CVAT client if successful.",
description="Accepts a request from Google with code and state query parameters. "
Expand All @@ -315,6 +335,24 @@ def google_oauth2_callback(*args, **kwargs):
return OAuth2CallbackViewEx.adapter_view(GoogleAdapter)(*args, **kwargs)


@extend_schema(
summary="Checks the authentication response from Amazon Cognito, redirects to the CVAT client if successful.",
description="Accepts a request from Amazon Cognito with code and state query parameters. "
"In case of successful authentication on the provider side, it will "
"redirect to the CVAT client",
parameters=[
OpenApiParameter('code', description='Returned by google',
location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
OpenApiParameter('state', description='Returned by google',
location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
],
)
@api_view(["GET"])
@permission_classes([AllowAny])
def amazon_cognito_oauth2_callback(*args, **kwargs):
return OAuth2CallbackViewEx.adapter_view(AmazonCognitoAdapter)(*args, **kwargs)


class ConfirmEmailViewEx(ConfirmEmailView):
template_name = 'account/email/email_confirmation_signup_message.html'

Expand Down Expand Up @@ -369,6 +407,10 @@ class GoogleLogin(SocialLoginViewEx):
client_class = OAuth2Client
callback_url = getattr(settings, 'GOOGLE_CALLBACK_URL', None)

class CognitoLogin(SocialLoginViewEx):
adapter_class = AmazonCognitoAdapter
client_class = OAuth2Client
callback_url = getattr(settings, 'AMAZON_COGNITO_REDIRECT_URI', None)

@extend_schema_view(
get=extend_schema(
Expand All @@ -379,6 +421,7 @@ class GoogleLogin(SocialLoginViewEx):
fields={
'google': SocialAuthMethodSerializer(),
'github': SocialAuthMethodSerializer(),
'amazon-cognito': SocialAuthMethodSerializer(),
}
)),
}
Expand All @@ -401,7 +444,7 @@ def get(self, request, *args, **kwargs):
getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_ID', None)
and getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_SECRET', None)
)
icon_path = osp.join(settings.STATIC_ROOT, 'social_authentication', f'social-{provider}-logo.svg')
icon_path = osp.join(settings.STATIC_ROOT, 'social_authentication', f'social-{provider.replace("_", "-")}-logo.svg')
if is_enabled and osp.exists(icon_path):
with open(icon_path, 'r') as f:
icon = f.read()
Expand All @@ -413,6 +456,6 @@ def get(self, request, *args, **kwargs):
})
serializer.is_valid(raise_exception=True)

response[provider] = serializer.validated_data
response[provider.replace("_", "-")] = serializer.validated_data

return Response(response)
2 changes: 1 addition & 1 deletion cvat/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Django==3.2.16
django-appconf==1.0.4
django-auth-ldap==2.2.0
django-compressor==2.4
dj-rest-auth[with_social]==2.2.5
django-rq==2.3.2
EasyProcess==0.3
Pillow==9.3.0
Expand Down Expand Up @@ -31,7 +32,6 @@ Pygments==2.7.4
drf-spectacular==0.22.1
Shapely==1.7.1
pdf2image==1.14.0
dj-rest-auth[with_social]==2.2.4
opencv-python-headless==4.5.5.62
h5py==3.6.0
django-cors-headers==3.5.0
Expand Down
26 changes: 24 additions & 2 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,21 @@ def add_ssh_keys():
'django_rq',
'compressor',
'django_sendfile',
"dj_rest_auth",
'dj_rest_auth.registration',
'dj_pagination',
'rest_framework',
'rest_framework.authtoken',
'drf_spectacular',
'dj_rest_auth',
'django.contrib.sites',
'allauth',
'allauth.account',
'corsheaders',
'allauth.socialaccount',
# social providers
'allauth.socialaccount.providers.amazon_cognito',
'allauth.socialaccount.providers.github',
'allauth.socialaccount.providers.google',
'dj_rest_auth.registration',
'health_check',
'health_check.db',
'health_check.contrib.migrations',
Expand Down Expand Up @@ -240,6 +241,7 @@ def add_ssh_keys():
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',

],
},
},
Expand Down Expand Up @@ -287,6 +289,7 @@ def add_ssh_keys():
# https://github.com/pennersr/django-allauth
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'

# set UI url to redirect after a successful e-mail confirmation
#changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/email-confirmation'
Expand Down Expand Up @@ -639,6 +642,7 @@ class CVAT_QUEUES(Enum):
SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx'
SOCIALACCOUNT_GITHUB_ADAPTER = 'cvat.apps.iam.adapters.GitHubAdapter'
SOCIALACCOUNT_GOOGLE_ADAPTER = 'cvat.apps.iam.adapters.GoogleAdapter'
SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER = 'cvat.apps.iam.adapters.AmazonCognitoOAuth2AdapterEx'
SOCIALACCOUNT_LOGIN_ON_GET = True
# It's required to define email in the case when a user has a private hidden email.
# (e.g in github account set keep my email addresses private)
Expand All @@ -648,6 +652,7 @@ class CVAT_QUEUES(Enum):
# custom variable because by default LOGIN_REDIRECT_URL will be used
SOCIAL_APP_LOGIN_REDIRECT_URL = f'{CVAT_BASE_URL}/auth/login-with-social-app'

AMAZON_COGNITO_REDIRECT_URI = f'{CVAT_BASE_URL}/api/auth/amazon-cognito/login/callback/'
GITHUB_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/github/login/callback/'
GOOGLE_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/google/login/callback/'

Expand All @@ -656,6 +661,13 @@ class CVAT_QUEUES(Enum):

SOCIAL_AUTH_GITHUB_CLIENT_ID = os.getenv('SOCIAL_AUTH_GITHUB_CLIENT_ID')
SOCIAL_AUTH_GITHUB_CLIENT_SECRET = os.getenv('SOCIAL_AUTH_GITHUB_CLIENT_SECRET')

SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID')
SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET')
SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN')

# Django allauth social account providers
# https://django-allauth.readthedocs.io/en/latest/providers.html
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
Expand All @@ -681,4 +693,14 @@ class CVAT_QUEUES(Enum):
# key with a capital letter will be used
'PUBLIC_NAME': 'GitHub',
},
'amazon_cognito': {
'DOMAIN': SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN,
'SCOPE': [ 'profile', 'email', 'openid'],
'APP': {
'client_id': SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID,
'secret': SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET,
'key': ''
},
'PUBLIC_NAME': 'Amazon Cognito',
}
}
1 change: 1 addition & 0 deletions cvat/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
if USE_ALLAUTH_SOCIAL_ACCOUNTS:
GITHUB_CALLBACK_URL = f'{UI_URL}/api/auth/github/login/callback/'
GOOGLE_CALLBACK_URL = f'{UI_URL}/api/auth/google/login/callback/'
AMAZON_COGNITO_REDIRECT_URI = f'{UI_URL}/api/auth/amazon-cognito/login/callback/'
SOCIALACCOUNT_CALLBACK_CANCELLED_URL = f'{UI_URL}/auth/login'
SOCIAL_APP_LOGIN_REDIRECT_URL = f'{UI_URL}/auth/login-with-social-app'
16 changes: 11 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ services:
IAM_OPA_BUNDLE: '1'
no_proxy: elasticsearch,kibana,logstash,nuclio,opa,${no_proxy:-}
NUMPROCS: 1
USE_ALLAUTH_SOCIAL_ACCOUNTS: ""
SOCIAL_AUTH_GOOGLE_CLIENT_ID: ""
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: ""
SOCIAL_AUTH_GITHUB_CLIENT_ID: ""
SOCIAL_AUTH_GITHUB_CLIENT_SECRET: ""
USE_ALLAUTH_SOCIAL_ACCOUNTS:
# Google enviroment variables
SOCIAL_AUTH_GOOGLE_CLIENT_ID:
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET:
# GitHub enviroment variables
SOCIAL_AUTH_GITHUB_CLIENT_ID:
SOCIAL_AUTH_GITHUB_CLIENT_SECRET:
# Amazon Cognito enviroment variables
SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN:
SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID:
SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET:
command: -c supervisord/server.conf
labels:
- traefik.enable=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ context('When clicking on the Logout button, get the user session closed.', () =
method: 'GET',
url: '/api/auth/social/methods/',
}).then((response) => {
socialAuthMethods = Object.keys(response.body);
socialAuthMethods = Object.keys(response.body).filter((item) => response.body[item].is_enabled);
expect(socialAuthMethods).length.gt(0);
cy.visit('auth/login');

Expand Down
15 changes: 15 additions & 0 deletions tests/python/mock_oauth2/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

from allauth.socialaccount.providers.amazon_cognito.views import AmazonCognitoOAuth2Adapter
from django.conf import settings


class TestAmazonCognitoOAuth2Adapter(AmazonCognitoOAuth2Adapter):
@property
def profile_url(self):
return super().profile_url.lower()

def get_callback_url(self, request, app):
return settings.AMAZON_COGNITO_REDIRECT_URI
Loading

0 comments on commit 0f0913c

Please sign in to comment.