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