diff --git a/documentation/assets/img/admin_get_emali_login.png b/documentation/assets/img/admin_get_emali_login.png new file mode 100644 index 00000000..2167bce6 Binary files /dev/null and b/documentation/assets/img/admin_get_emali_login.png differ diff --git a/documentation/assets/img/admin_successful_login_url.png b/documentation/assets/img/admin_successful_login_url.png new file mode 100644 index 00000000..c6774fe9 Binary files /dev/null and b/documentation/assets/img/admin_successful_login_url.png differ diff --git a/documentation/assets/img/magic_link.png b/documentation/assets/img/magic_link.png new file mode 100644 index 00000000..ebd95779 Binary files /dev/null and b/documentation/assets/img/magic_link.png differ diff --git a/documentation/docs/miscellaneous/authentication.md b/documentation/docs/miscellaneous/authentication.md new file mode 100644 index 00000000..cc6e1e6f --- /dev/null +++ b/documentation/docs/miscellaneous/authentication.md @@ -0,0 +1,30 @@ +# Authentication when Self-Hosting + +By default, most of the instructions for self-hosting Khoj assume a single user, and so the default configuration is to run in anonymous mode. However, if you want to enable authentication, you can do so either with [Google Auth](/miscellaneous/google_auth) or with magic links, as shown below. This can be helpful if you want to make sure your Khoj instance is only accessible to you and your team. + +:::tip[Note] +Remove the `--anonymous-mode` flag in your start up command to enable authentication. +::: + +The most secure way to do this is to integrate with [Resend](https://resend.com) by setting up an account and adding an environment variable for `RESEND_API_KEY`. You can get your API key [here](https://resend.com/api-keys). This will allow you to automatically send sign-in links to users who want to log in. + +It's still possible to use the magic links feature without Resend, but you'll need to manually send the magic links to users who want to log in. + +## Manually sending magic links + +1. The user will have to enter their email address in the login form. +They'll click `Send Magic Link`. Without the Resend API key, this will just create an unverified account for them in the backend +Magic link login form + +2. You can get their magic link using the admin panel +Go to the [admin panel](http://localhost:42110/server/admin/database/khojuser/). You'll see a list of users. Search for the user you want to send a magic link to. Tick the checkbox next to their row, and use the action drop down at the top to 'Get email login URL'. This will generate a magic link that you can send to the user, which will appear at the top of the admin interface. + +| Get email login URL | Retrieved login URL | +|---------------------|---------------------| +| Get user magic sign in link| Successfully retrieved a login URL| + +3. Send the magic link to the user. They can click on it to log in. + +Once they click on the link, they'll automatically be logged in. They'll have to repeat this process for every new device they want to log in from, but they shouldn't have to repeat it on the same device. + +A given magic link can only be used once. If the user tries to use it again, they'll be redirected to the login page to get a new magic link. diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 007ba09a..a0fc1be9 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -160,10 +160,14 @@ async def acreate_user_by_phone_number(phone_number: str) -> KhojUser: return user -async def get_or_create_user_by_email(email: str) -> KhojUser: +async def aget_or_create_user_by_email(email: str) -> KhojUser: user, _ = await KhojUser.objects.filter(email=email).aupdate_or_create(defaults={"username": email, "email": email}) await user.asave() + if user: + user.email_verification_code = secrets.token_urlsafe(18) + await user.asave() + user_subscription = await Subscription.objects.filter(user=user).afirst() if not user_subscription: await Subscription.objects.acreate(user=user, type="trial") @@ -171,10 +175,23 @@ async def get_or_create_user_by_email(email: str) -> KhojUser: return user +async def aget_user_validated_by_email_verification_code(code: str) -> KhojUser: + user = await KhojUser.objects.filter(email_verification_code=code).afirst() + if not user: + return None + + user.email_verification_code = None + user.verified_email = True + await user.asave() + + return user + + async def create_user_by_google_token(token: dict) -> KhojUser: user, _ = await KhojUser.objects.filter(email=token.get("email")).aupdate_or_create( defaults={"username": token.get("email"), "email": token.get("email")} ) + user.verified_email = True await user.asave() await GoogleUser.objects.acreate( @@ -228,7 +245,7 @@ async def set_user_subscription( email: str, is_recurring=None, renewal_date=None, type="standard" ) -> Optional[Subscription]: # Get or create the user object and their subscription - user = await get_or_create_user_by_email(email) + user = await aget_or_create_user_by_email(email) user_subscription = await Subscription.objects.filter(user=user).afirst() # Update the user subscription state diff --git a/src/khoj/database/admin.py b/src/khoj/database/admin.py index a96a57a0..64a0a7fd 100644 --- a/src/khoj/database/admin.py +++ b/src/khoj/database/admin.py @@ -2,7 +2,7 @@ import csv import json from apscheduler.job import Job -from django.contrib import admin +from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin from django.http import HttpResponse from django_apscheduler.admin import DjangoJobAdmin @@ -73,7 +73,19 @@ class KhojUserAdmin(UserAdmin): search_fields = ("email", "username", "phone_number", "uuid") filter_horizontal = ("groups", "user_permissions") - fieldsets = (("Personal info", {"fields": ("phone_number",)}),) + UserAdmin.fieldsets + fieldsets = (("Personal info", {"fields": ("phone_number", "email_verification_code")}),) + UserAdmin.fieldsets + + actions = ["get_email_login_url"] + + def get_email_login_url(self, request, queryset): + for user in queryset: + if user.email: + host = request.get_host() + unique_id = user.email_verification_code + login_url = f"{host}/auth/magic?code={unique_id}" + messages.info(request, f"Email login URL for {user.email}: {login_url}") + + get_email_login_url.short_description = "Get email login URL" # type: ignore admin.site.register(KhojUser, KhojUserAdmin) diff --git a/src/khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py b/src/khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py new file mode 100644 index 00000000..e7151b67 --- /dev/null +++ b/src/khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.11 on 2024-06-17 08:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0045_fileobject"), + ] + + operations = [ + migrations.AddField( + model_name="khojuser", + name="email_verification_code", + field=models.CharField(blank=True, default=None, max_length=200, null=True), + ), + migrations.AddField( + model_name="khojuser", + name="verified_email", + field=models.BooleanField(default=False), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 380dec22..52685471 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -32,6 +32,8 @@ class KhojUser(AbstractUser): uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False)) phone_number = PhoneNumberField(null=True, default=None, blank=True) verified_phone_number = models.BooleanField(default=False) + verified_email = models.BooleanField(default=False) + email_verification_code = models.CharField(max_length=200, null=True, default=None, blank=True) def save(self, *args, **kwargs): if not self.uuid: diff --git a/src/khoj/interface/email/magic_link.html b/src/khoj/interface/email/magic_link.html new file mode 100644 index 00000000..3cf9ecb6 --- /dev/null +++ b/src/khoj/interface/email/magic_link.html @@ -0,0 +1,17 @@ + + + + Welcome to Khoj + + + + + +

Hi! Click here to sign in on this browser.

+ +

- The Khoj Team

+ + + diff --git a/src/khoj/interface/web/login.html b/src/khoj/interface/web/login.html index 9b25e520..d9b24bb1 100644 --- a/src/khoj/interface/web/login.html +++ b/src/khoj/interface/web/login.html @@ -12,35 +12,74 @@
+ +
+ +
Login to Khoj
+ + + +
OR
+ +
+
+
+
+
- -
- -
Login to Khoj
- -
-
-
-
+
- - - - - + +