-
Notifications
You must be signed in to change notification settings - Fork 320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Placeholder] Implement "magic" login code #661
Comments
@iMerica I've tried to implement this in P.S.: I wouldn't mind receiving guidance as to what's needed at high level to implement this in the project (i.e.: references in the code, what checks need to happen and where they should be - serializers vs. views) I can think of a initial list:
Here's the scrappy implementation I've done on my side, which is currently working and might be useful if anyone wants to have a go at it:
# serializers.py
class RequestLoginCodeSerializer(serializers.Serializer):
"""
Serializer to validate the email address when requesting a login code.
"""
email = serializers.EmailField(required=True)
def validate_email(self, email):
email = get_adapter().clean_email(email)
if not EmailAddress.objects.filter(email__iexact=email).exists():
raise serializers.ValidationError(
_("No user is registered with this e-mail address.")
)
return email
class VerifyLoginCodeSerializer(serializers.Serializer):
code = serializers.CharField(required=True) # views.py
class RequestLoginCodeView(GenericAPIView):
serializer_class = RequestLoginCodeSerializer
permission_classes = (AllowAny,)
throttle_scope = "dj_rest_auth"
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
# Do not raise exception if the serializer is invalid
# This is to prevent users from knowing if an email is registered
if serializer.is_valid(raise_exception=False):
email = serializer.validated_data["email"]
flows.login_by_code.request_login_code(request, email)
# The response is the same regardless of whether the email is registered
return Response(
{"detail": _("If the email is registered, a login code will be sent.")},
status=status.HTTP_200_OK,
)
class VerifyLoginCodeView(GenericAPIView):
serializer_class = VerifyLoginCodeSerializer
permission_classes = (AllowAny,)
throttle_scope = "dj_rest_auth"
def post(self, request, *args, **kwargs):
# Get the code from the serializer
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
actual_code = serializer.validated_data["code"]
# If the user is authenticated, return a 400
if request.user.is_authenticated:
return Response(
{"detail": _("You are already logged in.")},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the login stage
login_stage = LoginStageController.enter(request, LoginByCodeStage.key)
if not login_stage:
return Response(
{"detail": _("The login code is invalid. Please request a new one.")},
status=status.HTTP_400_BAD_REQUEST,
)
user, pending_login = flows.login_by_code.get_pending_login(
self.request, login_stage.login, peek=True
)
if not pending_login:
return Response(
{"detail": _("The login code is invalid. Please request a new one.")},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the expected code from the pending session
expected_code = pending_login.get("code", "")
if not expected_code:
return Response(
{"detail": _("The login code is invalid. Please request a new one.")},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the code is valid
if not flows.login_by_code.compare_code(
actual=actual_code, expected=expected_code
):
# Record an invalid attempt
flows.login_by_code.record_invalid_attempt(request, login_stage.login)
return Response(
{
"detail": _(
"The login code is invalid. Please input the correct code."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
# Validate that the user is active
if not user.is_active:
return Response(
{"detail": _("The user account is disabled.")},
status=status.HTTP_400_BAD_REQUEST,
)
# Validate that the user has verified their email if required
from allauth.account import app_settings as allauth_account_settings
if (
allauth_account_settings.EMAIL_VERIFICATION
== allauth_account_settings.EmailVerificationMethod.MANDATORY
and not user.emailaddress_set.filter(
email=user.email, verified=True
).exists()
):
return Response(
{"detail": _("The user account has not been verified.")},
status=status.HTTP_400_BAD_REQUEST,
)
# Perform the login
flows.login_by_code.perform_login_by_code(request, login_stage, None)
# Return successful response
return Response(
{"detail": _("Ok")},
status=status.HTTP_200_OK,
) # settings.py
# ---------------------------------- ALLAUTH --------------------------------- #
# https://docs.allauth.org/en/latest/index.html
ACCOUNT_AUTHENTICATION_METHOD = "email" # username, email or username_email
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True # Needs to be True to use email as the auth method
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory" # "none", "optional", "mandatory"
ACCOUNT_LOGIN_BY_CODE_ENABLED = True # Enable login by Magic Code
# ACCOUNT_CONFIRM_EMAIL_ON_GET = True
# ACCOUNT_EMAIL_NOTIFICATIONS = True # This does not work yet with dj-rest-auth, it's handled in authentication.signals
SOCIALACCOUNT_QUERY_EMAIL = True # Needed to set email as the username
SOCIALACCOUNT_LOGIN_ON_GET = True # Needed to login with social accounts
SOCIALACCOUNT_STORE_TOKENS = True # Needed to access Provider's APIs
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = (
False # Do not allow unknown accounts to reset password
)
ACCOUNT_ADAPTER = "authentication.adapter.CustomAccountAdapter" # Override the default adapter to use a custom email module
CUSTOM_ACCOUNT_CONFIRM_EMAIL_URL = (
"/verifyemail/?key={0}" # An email verification URL that the client will pick up.
)
# Automatically log the user in after email confirmation
# This only works if the confirmation happens in the same browser
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
# Enable login by code
ACCOUNT_LOGIN_BY_CODE_ENABLED = True
ACCOUNT_LOGIN_BY_CODE_REQUIRED = False
ACCOUNT_LOGIN_BY_CODE_TIMEOUT = 60 * 15 # 15 minutes
ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS = 5
# allauth providers
# https://docs.allauth.org/en/latest/socialaccount/provider_configuration.html
SOCIALACCOUNT_PROVIDERS = {}
# Site ID is needed for allauth
SITE_ID = 1
# ------------------------------- DJ-REST-AUTH ------------------------------- #
# See defaults in https://dj-rest-auth.readthedocs.io/en/latest/configuration.html
REST_AUTH = {
# Use sessions instead of tokens
"TOKEN_MODEL": None,
"TOKEN_SERIALIZER": None,
"SESSION_LOGIN": True,
"LOGIN_SERIALIZER": "authentication.serializers.LoginSerializer",
"REGISTER_SERIALIZER": "authentication.serializers.RegisterSerializer",
"USER_DETAILS_SERIALIZER": "authentication.serializers.CustomUserDetailsSerializer",
"OLD_PASSWORD_FIELD_ENABLED": True,
} |
@dontic I am curious, why not just use |
Cause I didn't even know that was a even a thing 🤦♂️. This is great, reading the docs now. |
@pennersr just read the thing, this is amazing work. A couple of things to take into consideration:
We can also perhaps move this discussion elsewhere where it's more relevant - should we open an issue in https://github.com/pennersr/django-allauth and continue there? |
|
Makes total sense, didn't think of the Ninja guys. Keeping it agnostic is the way to go for sure.
Gotcha, thanks for the help!! |
Hello everyone. Am I correct in understanding that login by code in allauth is only available through Django forms? If I want to implement it through a REST API, I need to write my own viewsets and serializers similar to those @dontic shared above. If that’s the case, is there perhaps a need to add this functionality to dj-rest-auth? I could submit a pull request. My project currently requires this functionality, and I have implemented my own solution for it. |
No, there is a REST API for that: |
@pennersr thank you so much for this url! |
Earlier this year allauth released a code-based login method.
dj-rest-auth
's endpoint/login/code/
allauth
)dj-rest-auth
's endpoint/login/code/confirm/
This issue is a placeholder to keep track of the progress and discussion
Planning to create a pull request for this as soon as I can.
The text was updated successfully, but these errors were encountered: