Social account authentication tests (#5444)
Depends on #5349 Related #5432 Added tests for social account authentication functionality: cypress test with dummy auth servermain
parent
71a0aaf2bb
commit
b00bc653ff
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2022 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
interface SocialAuthMethodCamelCase {
|
||||||
|
provider: string;
|
||||||
|
publicName: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocialAuthMethodSnakeCase {
|
||||||
|
public_name: string;
|
||||||
|
is_enabled: boolean;
|
||||||
|
icon: string;
|
||||||
|
provider?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SocialAuthMethod {
|
||||||
|
public provider: string;
|
||||||
|
public publicName: string;
|
||||||
|
public isEnabled: boolean;
|
||||||
|
public icon: string;
|
||||||
|
|
||||||
|
constructor(initialData: SocialAuthMethodSnakeCase) {
|
||||||
|
const data: SocialAuthMethodCamelCase = {
|
||||||
|
provider: initialData.provider,
|
||||||
|
publicName: initialData.public_name,
|
||||||
|
isEnabled: initialData.is_enabled,
|
||||||
|
icon: initialData.icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperties(
|
||||||
|
this,
|
||||||
|
Object.freeze({
|
||||||
|
provider: {
|
||||||
|
get: () => data.provider,
|
||||||
|
},
|
||||||
|
publicName: {
|
||||||
|
get: () => data.publicName,
|
||||||
|
},
|
||||||
|
isEnabled: {
|
||||||
|
get: () => data.isEnabled,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
get: () => data.icon,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SocialAuthMethodsRawType = {
|
||||||
|
[index: string]: SocialAuthMethodSnakeCase;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SocialAuthMethods = SocialAuthMethod[];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership
|
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership
|
||||||
view,N/A,N/A,N/A,,GET,"/server/about, /server/annotation/formats, /server/plugins, /server/advanced-auth",None,N/A
|
view,N/A,N/A,N/A,,GET,"/server/about, /server/annotation/formats, /server/plugins",None,N/A
|
||||||
send:exception,N/A,N/A,N/A,,POST,/server/exception,None,N/A
|
send:exception,N/A,N/A,N/A,,POST,/server/exception,None,N/A
|
||||||
send:logs,N/A,N/A,N/A,,POST,/server/logs,None,N/A
|
send:logs,N/A,N/A,N/A,,POST,/server/logs,None,N/A
|
||||||
list:content,N/A,N/A,N/A,,GET,/server/share,Worker,N/A
|
list:content,N/A,N/A,N/A,,GET,/server/share,Worker,N/A
|
||||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
GOOGLE_SERVER_PORT=4320
|
||||||
|
GOOGLE_SERVER_HOST="test-google"
|
||||||
|
GITHUB_SERVER_PORT=4321
|
||||||
|
GITHUB_SERVER_HOST="test-github"
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
|
||||||
|
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
GOOGLE_SERVER_PORT = os.environ.get("GOOGLE_SERVER_PORT")
|
||||||
|
GOOGLE_SERVER_HOST = os.environ.get("GOOGLE_SERVER_HOST")
|
||||||
|
GITHUB_SERVER_PORT = os.environ.get("GITHUB_SERVER_PORT")
|
||||||
|
GITHUB_SERVER_HOST = os.environ.get("GITHUB_SERVER_HOST")
|
||||||
|
|
||||||
|
|
||||||
|
class TestGitHubAdapter(GitHubOAuth2Adapter):
|
||||||
|
access_token_url = (
|
||||||
|
f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/login/oauth/access_token" # nosec
|
||||||
|
)
|
||||||
|
authorize_url = f"http://localhost:{GITHUB_SERVER_PORT}/login/oauth/authorize"
|
||||||
|
profile_url = f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/user"
|
||||||
|
emails_url = f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/user/emails"
|
||||||
|
|
||||||
|
def get_callback_url(self, request, app):
|
||||||
|
return settings.GITHUB_CALLBACK_URL
|
||||||
|
|
||||||
|
|
||||||
|
class TestGoogleAdapter(GoogleOAuth2Adapter):
|
||||||
|
access_token_url = f"http://{GOOGLE_SERVER_HOST}:{GOOGLE_SERVER_PORT}/o/oauth2/token"
|
||||||
|
authorize_url = f"http://localhost:{GOOGLE_SERVER_PORT}/o/oauth2/auth"
|
||||||
|
profile_url = f"http://{GOOGLE_SERVER_HOST}:{GOOGLE_SERVER_PORT}/oauth2/v1/userinfo"
|
||||||
|
|
||||||
|
def get_callback_url(self, request, app):
|
||||||
|
return settings.GOOGLE_CALLBACK_URL
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
services:
|
||||||
|
cvat_server:
|
||||||
|
environment:
|
||||||
|
USE_ALLAUTH_SOCIAL_ACCOUNTS: "True"
|
||||||
|
SOCIAL_AUTH_GOOGLE_CLIENT_ID: "XXX"
|
||||||
|
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: "XXX"
|
||||||
|
SOCIAL_AUTH_GITHUB_CLIENT_ID: "XXX"
|
||||||
|
SOCIAL_AUTH_GITHUB_CLIENT_SECRET: "XXX"
|
||||||
|
DJANGO_SETTINGS_MODULE: social_auth.settings
|
||||||
|
GOOGLE_SERVER_HOST:
|
||||||
|
GOOGLE_SERVER_PORT:
|
||||||
|
GITHUB_SERVER_HOST:
|
||||||
|
GITHUB_SERVER_PORT:
|
||||||
|
volumes:
|
||||||
|
- ./tests/python/social_auth:/home/django/social_auth:ro
|
||||||
|
|
||||||
|
google_auth_server:
|
||||||
|
image: python:3.9-slim
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
GOOGLE_SERVER_HOST:
|
||||||
|
GOOGLE_SERVER_PORT:
|
||||||
|
ports:
|
||||||
|
- '${GOOGLE_SERVER_PORT}:${GOOGLE_SERVER_PORT}'
|
||||||
|
command: python3 /tmp/server.py --server "google"
|
||||||
|
volumes:
|
||||||
|
- ./tests/python/social_auth:/tmp
|
||||||
|
networks:
|
||||||
|
cvat:
|
||||||
|
aliases:
|
||||||
|
- ${GOOGLE_SERVER_HOST}
|
||||||
|
|
||||||
|
github_auth_server:
|
||||||
|
image: python:3.9-slim
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
GITHUB_SERVER_HOST:
|
||||||
|
GITHUB_SERVER_PORT:
|
||||||
|
ports:
|
||||||
|
- '${GITHUB_SERVER_PORT}:${GITHUB_SERVER_PORT}'
|
||||||
|
command: python3 /tmp/server.py --server "github"
|
||||||
|
volumes:
|
||||||
|
- ./tests/python/social_auth:/tmp
|
||||||
|
networks:
|
||||||
|
cvat:
|
||||||
|
aliases:
|
||||||
|
- ${GITHUB_SERVER_HOST}
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
# Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import datetime
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from random import choice, random, sample
|
||||||
|
from urllib.parse import parse_qsl, urlparse
|
||||||
|
|
||||||
|
|
||||||
|
class CommonRequestHandlerClass(BaseHTTPRequestHandler, ABC):
|
||||||
|
def _set_headers(self):
|
||||||
|
self.send_response(406)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(f"Unsupported request. Path: {self.path}".encode("utf8"))
|
||||||
|
|
||||||
|
def get_profile(self, token=None):
|
||||||
|
if not token:
|
||||||
|
self.send_response(403)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(self.PROFILE).encode("utf-8"))
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def authorize(self, query_params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_access_token(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_query(self, query_params):
|
||||||
|
supported_response_type = "code"
|
||||||
|
if not "client_id" in query_params:
|
||||||
|
self.send_response(400)
|
||||||
|
self.wfile.write("Client id not found in query params".encode("utf8"))
|
||||||
|
return
|
||||||
|
if not "redirect_uri" in query_params:
|
||||||
|
self.send_response(400)
|
||||||
|
self.wfile.write("Redirect uri not found".encode("utf8"))
|
||||||
|
return
|
||||||
|
if query_params.get("response_type", "code") != supported_response_type:
|
||||||
|
self.send_response(400)
|
||||||
|
self.wfile.write(
|
||||||
|
"Only code response type is supported by dummy auth server".encode("utf8")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
u = urlparse(self.path)
|
||||||
|
if u.path == self.AUTHORIZE_PATH:
|
||||||
|
return self.authorize(dict(parse_qsl(u.query)))
|
||||||
|
elif u.path == self.PROFILE_PATH:
|
||||||
|
token = self.headers.get("Authorization") or dict(parse_qsl(u.query)).get(
|
||||||
|
"access_token"
|
||||||
|
)
|
||||||
|
return self.get_profile(token)
|
||||||
|
self._set_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
u = urlparse(self.path)
|
||||||
|
if u.path == self.TOKEN_PATH:
|
||||||
|
return self.generate_access_token()
|
||||||
|
self._set_headers()
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRequestHandlerClass(CommonRequestHandlerClass):
|
||||||
|
AUTHORIZE_PATH = "/login/oauth/authorize"
|
||||||
|
PROFILE_PATH = "/user"
|
||||||
|
TOKEN_PATH = "/login/oauth/access_token"
|
||||||
|
|
||||||
|
CODE_LENGTH = 20
|
||||||
|
AUTH_TOKEN_LENGTH = 40
|
||||||
|
|
||||||
|
LOGIN = "test-user"
|
||||||
|
UID = int(random() * 100)
|
||||||
|
|
||||||
|
# demo profile not including all information returned by github
|
||||||
|
PROFILE = {
|
||||||
|
"login": LOGIN,
|
||||||
|
"id": UID,
|
||||||
|
"avatar_url": f"https://avatars.github.com/u/{UID}",
|
||||||
|
"url": f"https://api.github.com/users/{LOGIN}",
|
||||||
|
"html_url": f"https://github.com/{LOGIN}",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": False,
|
||||||
|
"name": "Test User",
|
||||||
|
"location": "Germany, Munich",
|
||||||
|
"email": "github.user@test.com",
|
||||||
|
"hireable": None,
|
||||||
|
"created_at": str(datetime.now()),
|
||||||
|
"updated_at": str(datetime.now()),
|
||||||
|
"two_factor_authentication": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def authorize(self, query_params):
|
||||||
|
super().check_query(query_params)
|
||||||
|
self.send_response(302)
|
||||||
|
redirect_to = query_params["redirect_uri"]
|
||||||
|
generated_code = "".join(sample(string.ascii_lowercase + string.digits, self.CODE_LENGTH))
|
||||||
|
|
||||||
|
# add query params
|
||||||
|
new_query = (
|
||||||
|
f"?code={generated_code}&state={query_params['state']}&"
|
||||||
|
f"scope={query_params['scope']}&promt=none"
|
||||||
|
)
|
||||||
|
redirect_to += new_query
|
||||||
|
self.send_header("Location", redirect_to)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def generate_access_token(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||||
|
generated_token = "".join(
|
||||||
|
sample(string.ascii_letters + string.digits, self.AUTH_TOKEN_LENGTH)
|
||||||
|
)
|
||||||
|
scope = "read:user,user:email"
|
||||||
|
content = f"access_token={generated_token}&scope={scope}&token_type=bearer".encode("utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleRequestHandlerClass(CommonRequestHandlerClass):
|
||||||
|
AUTHORIZE_PATH = "/o/oauth2/auth"
|
||||||
|
PROFILE_PATH = "/oauth2/v1/userinfo"
|
||||||
|
TOKEN_PATH = "/o/oauth2/token"
|
||||||
|
|
||||||
|
CODE_LENGTH = 70 # in real case 256 bytes
|
||||||
|
AUTH_TOKEN_LENGTH = 100 # in real case 2048 bytes
|
||||||
|
|
||||||
|
UID = int(random() * 100)
|
||||||
|
|
||||||
|
# demo profile not including all information returned by google
|
||||||
|
PROFILE = {
|
||||||
|
"id": UID,
|
||||||
|
"email": "google.user@gmail.com",
|
||||||
|
"verified_email": True,
|
||||||
|
"name": "Test User",
|
||||||
|
"given_name": "Test",
|
||||||
|
"family_name": "User",
|
||||||
|
"picture": f"https://avatars.google.com/u/{UID}",
|
||||||
|
"locale": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
def authorize(self, query_params):
|
||||||
|
super().check_query(query_params)
|
||||||
|
self.send_response(302)
|
||||||
|
redirect_to = query_params["redirect_uri"]
|
||||||
|
symbols = string.ascii_letters + string.digits
|
||||||
|
generated_code = "".join([choice(symbols) for i in range(self.CODE_LENGTH)])
|
||||||
|
|
||||||
|
# add query params
|
||||||
|
new_query = (
|
||||||
|
f"?code={generated_code}&state={query_params['state']}&"
|
||||||
|
f"scope={query_params['scope']}&promt=none"
|
||||||
|
)
|
||||||
|
redirect_to += new_query
|
||||||
|
self.send_header("Location", redirect_to)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def generate_access_token(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/json; charset=utf-8")
|
||||||
|
symbols = string.ascii_letters + string.digits + string.punctuation
|
||||||
|
generated_token = "".join([choice(symbols) for i in range(self.AUTH_TOKEN_LENGTH)])
|
||||||
|
id_token = "".join([choice(symbols) for i in range(self.AUTH_TOKEN_LENGTH)])
|
||||||
|
scope = "https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email"
|
||||||
|
content = {
|
||||||
|
"access_token": generated_token,
|
||||||
|
"expires_in": 3600, # 1 h
|
||||||
|
"scope": scope,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"id_token": id_token,
|
||||||
|
}
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(content).encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
class AuthServer:
|
||||||
|
SERVER_HOST = "0.0.0.0"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print(f"Starting dummy authentication server on {self.SERVER_HOST}, {self.SERVER_PORT}")
|
||||||
|
HTTPServer((self.SERVER_HOST, self.SERVER_PORT), self.REQUEST_HANDLER_CLASS).serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAuthServer(AuthServer):
|
||||||
|
SERVER_PORT = int(os.environ.get("GOOGLE_SERVER_PORT", "4320"))
|
||||||
|
REQUEST_HANDLER_CLASS = GoogleRequestHandlerClass
|
||||||
|
|
||||||
|
|
||||||
|
class GithubAuthServer(AuthServer):
|
||||||
|
SERVER_PORT = int(os.environ.get("GITHUB_SERVER_PORT", "4321"))
|
||||||
|
REQUEST_HANDLER_CLASS = GithubRequestHandlerClass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--server", choices=["google", "github"], type=str, default="google")
|
||||||
|
server = parser.parse_args().server
|
||||||
|
auth_servers = {
|
||||||
|
"google": GoogleAuthServer,
|
||||||
|
"github": GithubAuthServer,
|
||||||
|
}
|
||||||
|
server_class = auth_servers[server]
|
||||||
|
server_class().run()
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
# 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_GITHUB_ADAPTER = "social_auth.adapters.TestGitHubAdapter"
|
||||||
|
SOCIALACCOUNT_GOOGLE_ADAPTER = "social_auth.adapters.TestGoogleAdapter"
|
||||||
Loading…
Reference in New Issue