Django social account sso (#5059)

Issue: https://github.com/opencv/cvat/issues/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 <mday@EYEDIAGNOSIS.local>
Co-authored-by: Maria Khrustaleva <maria@cvat.ai>
Co-authored-by: Nikita Manovich <nikita@cvat.ai>
main
ddx-day 3 years ago committed by GitHub
parent 9b55a7f7d4
commit 0f0913c138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -94,6 +94,7 @@ jobs:
run: | run: |
docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_server/image.tar
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
-e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \
--entrypoint /bin/bash -u root cvat/server \ --entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml' -c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt pip3 install --user -r cvat-sdk/gen/requirements.txt

@ -97,6 +97,7 @@ jobs:
run: | run: |
docker load --input /tmp/cvat_server/image.tar docker load --input /tmp/cvat_server/image.tar
docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \
-e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \
--entrypoint /bin/bash -u root cvat/server \ --entrypoint /bin/bash -u root cvat/server \
-c 'python manage.py spectacular --file /transfer/schema.yml' -c 'python manage.py spectacular --file /transfer/schema.yml'
pip3 install --user -r cvat-sdk/gen/requirements.txt pip3 install --user -r cvat-sdk/gen/requirements.txt

@ -8,6 +8,7 @@ from django.conf import settings
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.account.adapter import DefaultAccountAdapter 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.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.exceptions import ImmediateHttpResponse from allauth.exceptions import ImmediateHttpResponse
@ -48,3 +49,7 @@ class GoogleAdapter(GoogleOAuth2Adapter):
def get_callback_url(self, request, app): def get_callback_url(self, request, app):
return settings.GOOGLE_CALLBACK_URL return settings.GOOGLE_CALLBACK_URL
class AmazonCognitoOAuth2AdapterEx(AmazonCognitoOAuth2Adapter):
def get_callback_url(self, request, app):
return settings.AMAZON_COGNITO_REDIRECT_URI

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="32" height="32" viewBox="-21.5 0 299 299" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M208.752,58.061 L234.523,51.425 L234.715,51.708 L235.366,207.315 L234.523,208.161 L229.213,208.388 L209.054,205.25 L208.752,204.456 L208.752,58.061" fill="#7A3E65">
</path>
<path d="M59.705,218.971 L59.8,218.978 L127.827,238.745 L128,238.878 L128.296,239.114 L128.2,298.346 L128,298.598 L59.705,265.42 L59.705,218.971" fill="#7A3E65">
</path>
<path d="M208.752,204.456 L128.112,223.768 L87.624,213.995 L59.705,218.971 L128,238.878 L233.405,210.341 L234.523,208.161 L208.752,204.456" fill="#CFB2C1">
</path>
<path d="M196.295,79.626 L195.638,78.877 L128.734,59.437 L128,59.72 L127.328,59.377 L22.052,89.734 L21.477,90.437 L22.322,90.9 L46.397,94.43 L47.248,94.141 L127.888,74.83 L168.376,84.603 L196.295,79.626" fill="#512843">
</path>
<path d="M47.248,240.537 L21.477,246.758 L21.432,246.609 L20.417,91.583 L21.477,90.437 L47.248,94.141 L47.248,240.537" fill="#C17B9E">
</path>
<path d="M82.04,180.403 L128,185.794 L128.345,185.279 L128.532,113.392 L128,112.803 L82.04,118.195 L82.04,180.403" fill="#7A3E65">
</path>
<path d="M173.96,180.403 L128,185.794 L128,112.803 L173.96,118.195 L173.96,180.403" fill="#C17B9E">
</path>
<path d="M196.295,79.626 L128,59.72 L128,0 L196.295,33.177 L196.295,79.626" fill="#C17B9E">
</path>
<path d="M128,0 L0,61.793 L0,236.804 L21.477,246.758 L21.477,90.437 L128,59.72 L128,0" fill="#7A3E65">
</path>
<path d="M234.523,51.425 L234.523,208.161 L128,238.878 L128,298.598 L256,236.804 L256,61.793 L234.523,51.425" fill="#C17B9E">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -12,15 +12,14 @@ from dj_rest_auth.views import (
from allauth.account import app_settings as allauth_settings from allauth.account import app_settings as allauth_settings
from cvat.apps.iam.views import ( from cvat.apps.iam.views import (
SigningView, RegisterViewEx, RulesView, ConfirmEmailViewEx, SigningView, CognitoLogin, RegisterViewEx, RulesView,
) ConfirmEmailViewEx, LoginViewEx, GitHubLogin, GoogleLogin, SocialAuthMethods,
from cvat.apps.iam.views import (
github_oauth2_login as github_login, github_oauth2_login as github_login,
github_oauth2_callback as github_callback, github_oauth2_callback as github_callback,
google_oauth2_login as google_login, google_oauth2_login as google_login,
google_oauth2_callback as google_callback, google_oauth2_callback as google_callback,
LoginViewEx, GitHubLogin, GoogleLogin, amazon_cognito_oauth2_login as amazon_cognito_login,
SocialAuthMethods, amazon_cognito_oauth2_callback as amazon_cognito_callback,
) )
urlpatterns = [ urlpatterns = [
@ -58,6 +57,9 @@ if settings.IAM_TYPE == 'BASIC':
path('google/login/', google_login, name='google_login'), path('google/login/', google_login, name='google_login'),
path('google/login/callback/', google_callback, name='google_callback'), path('google/login/callback/', google_callback, name='google_callback'),
path('google/login/token', GoogleLogin.as_view()), 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))] urlpatterns = [path('auth/', include(urlpatterns))]

@ -28,13 +28,13 @@ from allauth.socialaccount.models import SocialLogin
from allauth.socialaccount.providers.oauth2.views import OAuth2CallbackView, OAuth2LoginView from allauth.socialaccount.providers.oauth2.views import OAuth2CallbackView, OAuth2LoginView
from allauth.socialaccount.providers.oauth2.client import OAuth2Client from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.utils import get_request_param from allauth.utils import get_request_param
from furl import furl from furl import furl
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, OpenApiParameter, extend_schema, inline_serializer, extend_schema_view 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 drf_spectacular.contrib.rest_auth import get_token_serializer_class
from cvat.apps.iam.adapters import GitHubAdapter, GoogleAdapter
from .authentication import Signer from .authentication import Signer
from cvat.apps.iam.serializers import SocialLoginSerializerEx, SocialAuthMethodSerializer from cvat.apps.iam.serializers import SocialLoginSerializerEx, SocialAuthMethodSerializer
@ -49,6 +49,12 @@ GoogleAdapter = (
else None else None
) )
AmazonCognitoAdapter = (
import_callable(settings.SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER)
if settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
else None
)
def get_context(request): def get_context(request):
from cvat.apps.organizations.models import Organization, Membership from cvat.apps.organizations.models import Organization, Membership
@ -251,8 +257,11 @@ class OAuth2CallbackViewEx(OAuth2CallbackView):
if not code: if not code:
return HttpResponseBadRequest('Parameter code not found in request') return HttpResponseBadRequest('Parameter code not found in request')
provider = self.adapter.provider_id.replace('_', '-')
return HttpResponseRedirect( 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'&auth_params={state.get("auth_params")}&process={state.get("process")}'
f'&scope={state.get("scope")}') f'&scope={state.get("scope")}')
@ -297,6 +306,17 @@ def github_oauth2_callback(*args, **kwargs):
def google_oauth2_login(*args, **kwargs): def google_oauth2_login(*args, **kwargs):
return OAuth2LoginView.adapter_view(GoogleAdapter)(*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( @extend_schema(
summary="Checks the authentication response from Google, redirects to the CVAT client if successful.", 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. " description="Accepts a request from Google with code and state query parameters. "
@ -315,6 +335,24 @@ def google_oauth2_callback(*args, **kwargs):
return OAuth2CallbackViewEx.adapter_view(GoogleAdapter)(*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): class ConfirmEmailViewEx(ConfirmEmailView):
template_name = 'account/email/email_confirmation_signup_message.html' template_name = 'account/email/email_confirmation_signup_message.html'
@ -369,6 +407,10 @@ class GoogleLogin(SocialLoginViewEx):
client_class = OAuth2Client client_class = OAuth2Client
callback_url = getattr(settings, 'GOOGLE_CALLBACK_URL', None) 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( @extend_schema_view(
get=extend_schema( get=extend_schema(
@ -379,6 +421,7 @@ class GoogleLogin(SocialLoginViewEx):
fields={ fields={
'google': SocialAuthMethodSerializer(), 'google': SocialAuthMethodSerializer(),
'github': SocialAuthMethodSerializer(), 'github': SocialAuthMethodSerializer(),
'amazon-cognito': SocialAuthMethodSerializer(),
} }
)), )),
} }
@ -401,7 +444,7 @@ class SocialAuthMethods(views.APIView):
getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_ID', None) getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_ID', None)
and getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_SECRET', 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): if is_enabled and osp.exists(icon_path):
with open(icon_path, 'r') as f: with open(icon_path, 'r') as f:
icon = f.read() icon = f.read()
@ -413,6 +456,6 @@ class SocialAuthMethods(views.APIView):
}) })
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
response[provider] = serializer.validated_data response[provider.replace("_", "-")] = serializer.validated_data
return Response(response) return Response(response)

@ -4,6 +4,7 @@ Django==3.2.16
django-appconf==1.0.4 django-appconf==1.0.4
django-auth-ldap==2.2.0 django-auth-ldap==2.2.0
django-compressor==2.4 django-compressor==2.4
dj-rest-auth[with_social]==2.2.5
django-rq==2.3.2 django-rq==2.3.2
EasyProcess==0.3 EasyProcess==0.3
Pillow==9.3.0 Pillow==9.3.0
@ -31,7 +32,6 @@ Pygments==2.7.4
drf-spectacular==0.22.1 drf-spectacular==0.22.1
Shapely==1.7.1 Shapely==1.7.1
pdf2image==1.14.0 pdf2image==1.14.0
dj-rest-auth[with_social]==2.2.4
opencv-python-headless==4.5.5.62 opencv-python-headless==4.5.5.62
h5py==3.6.0 h5py==3.6.0
django-cors-headers==3.5.0 django-cors-headers==3.5.0

@ -111,20 +111,21 @@ INSTALLED_APPS = [
'django_rq', 'django_rq',
'compressor', 'compressor',
'django_sendfile', 'django_sendfile',
"dj_rest_auth",
'dj_rest_auth.registration',
'dj_pagination', 'dj_pagination',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'dj_rest_auth',
'django.contrib.sites', 'django.contrib.sites',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'corsheaders', 'corsheaders',
'allauth.socialaccount', 'allauth.socialaccount',
# social providers # social providers
'allauth.socialaccount.providers.amazon_cognito',
'allauth.socialaccount.providers.github', 'allauth.socialaccount.providers.github',
'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.google',
'dj_rest_auth.registration',
'health_check', 'health_check',
'health_check.db', 'health_check.db',
'health_check.contrib.migrations', 'health_check.contrib.migrations',
@ -240,6 +241,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
], ],
}, },
}, },
@ -287,6 +289,7 @@ AUTHENTICATION_BACKENDS = [
# https://github.com/pennersr/django-allauth # https://github.com/pennersr/django-allauth
ACCOUNT_EMAIL_VERIFICATION = 'none' ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
# set UI url to redirect after a successful e-mail confirmation # set UI url to redirect after a successful e-mail confirmation
#changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message #changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/email-confirmation' ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/email-confirmation'
@ -639,6 +642,7 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx' SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx'
SOCIALACCOUNT_GITHUB_ADAPTER = 'cvat.apps.iam.adapters.GitHubAdapter' SOCIALACCOUNT_GITHUB_ADAPTER = 'cvat.apps.iam.adapters.GitHubAdapter'
SOCIALACCOUNT_GOOGLE_ADAPTER = 'cvat.apps.iam.adapters.GoogleAdapter' SOCIALACCOUNT_GOOGLE_ADAPTER = 'cvat.apps.iam.adapters.GoogleAdapter'
SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER = 'cvat.apps.iam.adapters.AmazonCognitoOAuth2AdapterEx'
SOCIALACCOUNT_LOGIN_ON_GET = True SOCIALACCOUNT_LOGIN_ON_GET = True
# It's required to define email in the case when a user has a private hidden email. # 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) # (e.g in github account set keep my email addresses private)
@ -648,6 +652,7 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
# custom variable because by default LOGIN_REDIRECT_URL will be used # 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' 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/' GITHUB_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/github/login/callback/'
GOOGLE_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/google/login/callback/' GOOGLE_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/google/login/callback/'
@ -656,6 +661,13 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIAL_AUTH_GITHUB_CLIENT_ID = os.getenv('SOCIAL_AUTH_GITHUB_CLIENT_ID') 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_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 = { SOCIALACCOUNT_PROVIDERS = {
'google': { 'google': {
'APP': { 'APP': {
@ -681,4 +693,14 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
# key with a capital letter will be used # key with a capital letter will be used
'PUBLIC_NAME': 'GitHub', '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',
}
} }

@ -50,5 +50,6 @@ IAM_OPA_DATA_URL = f'{IAM_OPA_HOST}/v1/data'
if USE_ALLAUTH_SOCIAL_ACCOUNTS: if USE_ALLAUTH_SOCIAL_ACCOUNTS:
GITHUB_CALLBACK_URL = f'{UI_URL}/api/auth/github/login/callback/' GITHUB_CALLBACK_URL = f'{UI_URL}/api/auth/github/login/callback/'
GOOGLE_CALLBACK_URL = f'{UI_URL}/api/auth/google/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' SOCIALACCOUNT_CALLBACK_CANCELLED_URL = f'{UI_URL}/auth/login'
SOCIAL_APP_LOGIN_REDIRECT_URL = f'{UI_URL}/auth/login-with-social-app' SOCIAL_APP_LOGIN_REDIRECT_URL = f'{UI_URL}/auth/login-with-social-app'

@ -40,11 +40,17 @@ services:
IAM_OPA_BUNDLE: '1' IAM_OPA_BUNDLE: '1'
no_proxy: elasticsearch,kibana,logstash,nuclio,opa,${no_proxy:-} no_proxy: elasticsearch,kibana,logstash,nuclio,opa,${no_proxy:-}
NUMPROCS: 1 NUMPROCS: 1
USE_ALLAUTH_SOCIAL_ACCOUNTS: "" USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIAL_AUTH_GOOGLE_CLIENT_ID: "" # Google enviroment variables
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: "" SOCIAL_AUTH_GOOGLE_CLIENT_ID:
SOCIAL_AUTH_GITHUB_CLIENT_ID: "" SOCIAL_AUTH_GOOGLE_CLIENT_SECRET:
SOCIAL_AUTH_GITHUB_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 command: -c supervisord/server.conf
labels: labels:
- traefik.enable=true - traefik.enable=true

@ -103,7 +103,7 @@ context('When clicking on the Logout button, get the user session closed.', () =
method: 'GET', method: 'GET',
url: '/api/auth/social/methods/', url: '/api/auth/social/methods/',
}).then((response) => { }).then((response) => {
socialAuthMethods = Object.keys(response.body); socialAuthMethods = Object.keys(response.body).filter((item) => response.body[item].is_enabled);
expect(socialAuthMethods).length.gt(0); expect(socialAuthMethods).length.gt(0);
cy.visit('auth/login'); cy.visit('auth/login');

@ -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

@ -0,0 +1,24 @@
{
"interactiveLogin": true,
"httpServer": "NettyWrapper",
"tokenCallbacks": [
{
"issuerId": "oauth2",
"tokenExpiry": 120,
"requestMappings": [
{
"requestParam": "client_id",
"match": "test-client",
"claims": {
"sub": "test-sub",
"aud": [
"test-aud"
],
"email": "admin@localhost.company",
"email_verified": true
}
}
]
}
]
}

@ -0,0 +1,23 @@
services:
mock_oauth2:
image: ghcr.io/navikt/mock-oauth2-server:0.5.3
environment:
JSON_CONFIG_PATH: "/devel/config.json"
SERVER_PORT: 9999
ports:
- 9999:9999
volumes:
- ./tests/python/mock_oauth2:/devel
networks:
- cvat
cvat_server:
environment:
USE_ALLAUTH_SOCIAL_ACCOUNTS: "True"
SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN: "http://mock_oauth2:9999"
SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID: "test-client"
DJANGO_SETTINGS_MODULE: mock_oauth2.settings
volumes:
- ./tests/python/mock_oauth2:/home/django/mock_oauth2:ro
depends_on:
- mock_oauth2

@ -0,0 +1,10 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
from cvat.settings.production import *
ACCOUNT_EMAIL_REQUIRED = True
if USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER = "mock_oauth2.adapters.TestAmazonCognitoOAuth2Adapter"

@ -8,7 +8,7 @@ from http import HTTPStatus
import pytest import pytest
from cvat_sdk.api_client import ApiClient, Configuration, models from cvat_sdk.api_client import ApiClient, Configuration, models
from shared.utils.config import BASE_URL, USER_PASS, make_api_client from shared.utils.config import BASE_URL, IS_AMAZON_COGNITO_AUTH_ENABLED, USER_PASS, make_api_client
@pytest.mark.usefixtures("restore_db_per_class") @pytest.mark.usefixtures("restore_db_per_class")
@ -21,6 +21,22 @@ class TestBasicAuth:
assert response.status == HTTPStatus.OK assert response.status == HTTPStatus.OK
assert user.username == username assert user.username == username
@pytest.mark.with_external_services
@pytest.mark.skipif(
not IS_AMAZON_COGNITO_AUTH_ENABLED, reason="Amazon Cognito authentication is disabled"
)
def test_can_do_basic_cognito_token_auth(self):
config = Configuration(host=BASE_URL)
with ApiClient(config) as client:
social_login_request = models.SocialLoginSerializerExRequest()
social_login_request.code = "test-code"
(auth, _) = client.auth_api.create_amazon_cognito_login_token(
social_login_serializer_ex_request=social_login_request
)
assert "sessionid" in client.cookies
assert "csrftoken" in client.cookies
assert auth.key
@pytest.mark.usefixtures("restore_db_per_function") @pytest.mark.usefixtures("restore_db_per_function")
class TestTokenAuth: class TestTokenAuth:

@ -29,15 +29,24 @@ CONTAINER_NAME_FILES = [
) )
] ]
DC_FILES = [ # this files contain some configurations that override the default configuration of the main containers
CVAT_ROOT_DIR / dc_file DC_OVERRIDE_FILES = [
for dc_file in ( CVAT_ROOT_DIR / "tests/python/mock_oauth2/docker-compose.yml",
"docker-compose.dev.yml", ]
"tests/docker-compose.file_share.yml",
"tests/docker-compose.minio.yml", DC_FILES = (
"tests/docker-compose.test_servers.yml", [
) CVAT_ROOT_DIR / dc_file
] + CONTAINER_NAME_FILES for dc_file in (
"docker-compose.dev.yml",
"tests/docker-compose.file_share.yml",
"tests/docker-compose.minio.yml",
"tests/docker-compose.test_servers.yml",
)
]
+ CONTAINER_NAME_FILES
+ DC_OVERRIDE_FILES
)
def pytest_addoption(parser): def pytest_addoption(parser):

@ -20,6 +20,9 @@ MINIO_SECRET_KEY = "minio_secret_key" # nosec
MINIO_ENDPOINT_URL = "http://localhost:9000" MINIO_ENDPOINT_URL = "http://localhost:9000"
IS_AMAZON_COGNITO_AUTH_ENABLED = True
def _to_query_params(**kwargs): def _to_query_params(**kwargs):
return "&".join([f"{k}={v}" for k, v in kwargs.items()]) return "&".join([f"{k}={v}" for k, v in kwargs.items()])

Loading…
Cancel
Save