auth: Refactor social login rendering.

login_context now gets the social_backends list through
get_social_backend_dicts and we  move display_logo customization
to backend class definition.

This prepares for easily adding multiple IdP support in SAML
authentication - there will be a social_backend dict for each configured
IdP, also allowing display_name and icon customization per IdP.
This commit is contained in:
Mateusz Mandera
2019-10-22 18:11:28 +02:00
committed by Tim Abbott
parent 9532e99800
commit 28dd1b34f2
8 changed files with 49 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -632,36 +632,6 @@ button.login-social-button:active {
box-shadow: 0px 1px 1px hsla(0, 0%, 0%, 0.3); box-shadow: 0px 1px 1px hsla(0, 0%, 0%, 0.3);
} }
.saml-wrapper button.login-social-button {
background-image: url('/static/images/landing-page/logos/saml-icon.png');
width: 100%;
}
.google-wrapper button.login-social-button {
background-image: url('/static/images/landing-page/logos/googl_e-icon.png');
width: 100%;
}
.github-wrapper::before {
content: "\f09b";
position: absolute;
font-family: "FontAwesome";
font-size: 2rem;
color: hsl(0, 0%, 20%);
transform: translateX(15px) translateY(13px);
}
.azuread-oauth2-wrapper::before {
content: "\f17a";
position: absolute;
font-family: "FontAwesome";
font-size: 2rem;
color: hsl(0, 0%, 20%);
transform: translateX(15px) translateY(13px);
}
.login-page-container .right-side .actions, .login-page-container .right-side .actions,
.forgot-password-container .actions { .forgot-password-container .actions {
margin: 20px 0px 0px; margin: 20px 0px 0px;

View File

@@ -75,9 +75,9 @@ page can be easily identified in it's respective JavaScript file -->
{% for backend in social_backends %} {% for backend in social_backends %}
<div class="login-social"> <div class="login-social">
<form class="form-inline {{ backend.name }}-wrapper" action="{{ backend.signup_url }}" method="get"> <form class="form-inline" action="{{ backend.signup_url }}" method="get">
<input type='hidden' name='multiuse_object_key' value='{{ multiuse_object_key }}' /> <input type='hidden' name='multiuse_object_key' value='{{ multiuse_object_key }}' />
<button class="login-social-button full-width"> <button class="login-social-button full-width" style="background-image:url({{ backend.display_logo }})">
{{ _('Sign up with %(identity_provider)s', identity_provider=backend.display_name) }} {{ _('Sign up with %(identity_provider)s', identity_provider=backend.display_name) }}
</button> </button>
</form> </form>

View File

@@ -132,9 +132,9 @@ page can be easily identified in it's respective JavaScript file. -->
{% for backend in social_backends %} {% for backend in social_backends %}
<div class="login-social"> <div class="login-social">
<form class="social_login_form form-inline {{ backend.name }}-wrapper" action="{{ backend.login_url }}" method="get"> <form class="social_login_form form-inline" action="{{ backend.login_url }}" method="get">
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
<button class="login-social-button"> <button class="login-social-button" style="background-image:url({{ backend.display_logo }})">
{{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }} {{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }}
</button> </button>
</form> </form>

View File

@@ -676,6 +676,10 @@ html_rules = whitespace_rules + prose_style_rules + [
'templates/zerver/email.html', 'templates/zerver/email.html',
'templates/zerver/email_log.html', 'templates/zerver/email_log.html',
# Social backend logos are dynamically loaded
'templates/zerver/accounts_home.html',
'templates/zerver/login.html',
# Probably just needs to be changed to display: none so the exclude works # Probably just needs to be changed to display: none so the exclude works
'templates/zerver/app/navbar.html', 'templates/zerver/app/navbar.html',

View File

@@ -3,16 +3,15 @@ from urllib.parse import urljoin
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings from django.conf import settings
from django.urls import reverse
from zerver.models import UserProfile, get_realm, Realm from zerver.models import UserProfile, get_realm, Realm
from zproject.backends import ( from zproject.backends import (
any_social_backend_enabled, any_social_backend_enabled,
get_social_backend_dicts,
password_auth_enabled, password_auth_enabled,
require_email_format_usernames, require_email_format_usernames,
auth_enabled_helper, auth_enabled_helper,
AUTH_BACKEND_NAME_MAP, AUTH_BACKEND_NAME_MAP,
SOCIAL_AUTH_BACKENDS,
) )
from zerver.decorator import get_client_name from zerver.decorator import get_client_name
from zerver.lib.send_email import FromAddress from zerver.lib.send_email import FromAddress
@@ -168,7 +167,6 @@ def login_context(request: HttpRequest) -> Dict[str, Any]:
# Add the keys for our standard authentication backends. # Add the keys for our standard authentication backends.
no_auth_enabled = True no_auth_enabled = True
social_backends = []
for auth_backend_name in AUTH_BACKEND_NAME_MAP: for auth_backend_name in AUTH_BACKEND_NAME_MAP:
name_lower = auth_backend_name.lower() name_lower = auth_backend_name.lower()
key = "%s_auth_enabled" % (name_lower,) key = "%s_auth_enabled" % (name_lower,)
@@ -177,19 +175,7 @@ def login_context(request: HttpRequest) -> Dict[str, Any]:
if is_enabled: if is_enabled:
no_auth_enabled = False no_auth_enabled = False
# Now add the enabled social backends to the social_backends context['social_backends'] = get_social_backend_dicts(realm)
# list used to generate buttons for login/register pages.
backend = AUTH_BACKEND_NAME_MAP[auth_backend_name]
if not is_enabled or backend not in SOCIAL_AUTH_BACKENDS:
continue
social_backends.append({
'name': backend.name,
'display_name': backend.auth_backend_name,
'login_url': reverse('login-social', args=(backend.name,)),
'signup_url': reverse('signup-social', args=(backend.name,)),
'sort_order': backend.sort_order,
})
context['social_backends'] = sorted(social_backends, key=lambda x: x['sort_order'], reverse=True)
context['no_auth_enabled'] = no_auth_enabled context['no_auth_enabled'] = no_auth_enabled
return context return context

View File

@@ -17,6 +17,7 @@ import logging
import magic import magic
import ujson import ujson
from typing import Any, Dict, List, Optional, Set, Tuple, Union from typing import Any, Dict, List, Optional, Set, Tuple, Union
from typing_extensions import TypedDict
from django_auth_ldap.backend import LDAPBackend, LDAPReverseEmailSearch, \ from django_auth_ldap.backend import LDAPBackend, LDAPReverseEmailSearch, \
_LDAPUser, ldap_error _LDAPUser, ldap_error
@@ -992,6 +993,9 @@ def social_auth_finish(backend: Any,
class SocialAuthMixin(ZulipAuthMixin): class SocialAuthMixin(ZulipAuthMixin):
auth_backend_name = "undeclared" auth_backend_name = "undeclared"
name = "undeclared"
display_logo = None # type: Optional[str]
# Used to determine how to order buttons on login form, backend with # Used to determine how to order buttons on login form, backend with
# higher sort order are displayed first. # higher sort order are displayed first.
sort_order = 0 sort_order = 0
@@ -1020,8 +1024,10 @@ class SocialAuthMixin(ZulipAuthMixin):
return None return None
class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2): class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2):
name = "github"
auth_backend_name = "GitHub" auth_backend_name = "GitHub"
sort_order = 100 sort_order = 100
display_logo = "/static/images/landing-page/logos/github-icon.png"
def get_verified_emails(self, *args: Any, **kwargs: Any) -> List[str]: def get_verified_emails(self, *args: Any, **kwargs: Any) -> List[str]:
access_token = kwargs["response"]["access_token"] access_token = kwargs["response"]["access_token"]
@@ -1085,12 +1091,15 @@ class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2):
class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2): class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2):
sort_order = 50 sort_order = 50
name = "azuread-oauth2"
auth_backend_name = "AzureAD" auth_backend_name = "AzureAD"
display_logo = "/static/images/landing-page/logos/azuread-icon.png"
class GoogleAuthBackend(SocialAuthMixin, GoogleOAuth2): class GoogleAuthBackend(SocialAuthMixin, GoogleOAuth2):
sort_order = 150 sort_order = 150
auth_backend_name = "Google" auth_backend_name = "Google"
name = "google" name = "google"
display_logo = "/static/images/landing-page/logos/googl_e-icon.png"
def get_verified_emails(self, *args: Any, **kwargs: Any) -> List[str]: def get_verified_emails(self, *args: Any, **kwargs: Any) -> List[str]:
verified_emails = [] # type: List[str] verified_emails = [] # type: List[str]
@@ -1105,10 +1114,12 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
standard_relay_params = ["subdomain", "multiuse_object_key", "mobile_flow_otp", standard_relay_params = ["subdomain", "multiuse_object_key", "mobile_flow_otp",
"next", "is_signup"] "next", "is_signup"]
REDIS_EXPIRATION_SECONDS = 60 * 15 REDIS_EXPIRATION_SECONDS = 60 * 15
name = "saml"
# Organization which go through the trouble of setting up SAML are most likely # Organization which go through the trouble of setting up SAML are most likely
# to have it as their main authentication method, so it seems appropriate to have # to have it as their main authentication method, so it seems appropriate to have
# SAML buttons at the top. # SAML buttons at the top.
sort_order = 9999 sort_order = 9999
display_logo = "/static/images/landing-page/logos/saml-icon.png"
def auth_url(self) -> str: def auth_url(self) -> str:
"""Get the URL to which we must redirect in order to """Get the URL to which we must redirect in order to
@@ -1222,6 +1233,34 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
return result return result
SocialBackendDictT = TypedDict('SocialBackendDictT', {
'name': str,
'display_name': str,
'display_logo': str,
'login_url': str,
'signup_url': str,
'sort_order': int,
})
def create_standard_social_backend_dict(social_backend: SocialAuthMixin) -> SocialBackendDictT:
assert social_backend.display_logo is not None
return dict(
name=social_backend.name,
display_name=social_backend.auth_backend_name,
display_logo=social_backend.display_logo,
login_url=reverse('login-social', args=(social_backend.name,)),
signup_url=reverse('signup-social', args=(social_backend.name,)),
sort_order=social_backend.sort_order
)
def get_social_backend_dicts(realm: Optional[Realm]=None) -> List[SocialBackendDictT]:
result = []
for backend in SOCIAL_AUTH_BACKENDS:
if auth_enabled_helper([backend.auth_backend_name], realm):
result.append(create_standard_social_backend_dict(backend))
return sorted(result, key=lambda x: x['sort_order'], reverse=True)
AUTH_BACKEND_NAME_MAP = { AUTH_BACKEND_NAME_MAP = {
'Dev': DevAuthBackend, 'Dev': DevAuthBackend,
'Email': EmailAuthBackend, 'Email': EmailAuthBackend,