django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

How do I perform google social authentication?

Open Jorncg opened this issue 3 years ago • 24 comments
trafficstars

I need to implement a social login for my api with django ninja, but i don't find any example for this.

what is the best approach?

Jorncg avatar Jun 22 '22 01:06 Jorncg

I think this repo may helps you to implement it. just try to call authentication third-parity API in your own endpoint API that implemented by ninja

alirezapla avatar Jun 23 '22 05:06 alirezapla

You can still use django-allauth. I guess it's best way to implement social login. One day, I will implement it too as like u. At that day, I will post some tutorial for you. Please be patient. If you don't have much time, I leave some links you can reference.

https://django-allauth.readthedocs.io/en/latest/installation.html https://django-allauth.readthedocs.io/en/latest/faq.html#this-information-is-nice-and-all-but-i-need-more https://testdriven.io/blog/django-social-auth/

quroom avatar Jul 04 '22 02:07 quroom

Hello, Did somebody successfully implemented python-social-auth with django ninja ?

yleclanche avatar Sep 11 '23 14:09 yleclanche

I implemented it with dj-rest-auth. It's more restful package. Look. https://testdriven.io/blog/django-rest-auth/

This can be helpful.

quroom avatar Sep 11 '23 21:09 quroom

I am searching for a way as well... so far I did not found any repo or package that can help with that

Nils3311 avatar Sep 28 '23 18:09 Nils3311

@Nils3311 Social-nets authentication is not API/OpenAPI specific

to implement it you can use any django app that does it with regular html pages - and then use session auth in NinjaAPI

f.e allauth - https://django-allauth.readthedocs.io/en/latest/

vitalik avatar Sep 29 '23 07:09 vitalik

I finally did it with python-social-auth.

After login, I redirect the user to /login/success (on my backend) :

In settings :

LOGIN_REDIRECT_URL = f"/login/success"

Then in the view, I set the API token and redirect to my front :

@session_auth_router.get("/login/success")
def login_success(request):
    response = HttpResponseRedirect(settings.FRONT_URL + "/play")
    response.set_cookie("api_token", request.user.token)
    return response

My whole API use a TokenAuth router, so I had to create a new session auth just for this view (so request.user is set in the previous view) :

session_auth_router = Router(auth=SessionAuth())

yleclanche avatar Sep 29 '23 07:09 yleclanche

@yleclanche Thanks for sharing.

quroom avatar Nov 09 '23 03:11 quroom

looking to implement allauth with JWT

vic-cieslak avatar Jan 18 '24 14:01 vic-cieslak

@Nils3311 Social-nets authentication is not API/OpenAPI specific

to implement it you can use any django app that does it with regular html pages - and then use session auth in NinjaAPI

f.e allauth - https://django-allauth.readthedocs.io/en/latest/

Hi @vitalik congrats on the framework, it seems like it's become a go-to in Django. I think it would help a lot of people such as myself if you could please explain a little further even if it's just some broad steps.

I currently have django-allauth and its html templates working well, and I'm using django-ninja's ninja.security.django_auth. Now I would like to use this from my frontend (React). I'm seeing django-allauth is logging the following steps:

"GET /accounts/google/login/?process=login HTTP/1.1" 200 1246
"POST /accounts/google/login/?process=login HTTP/1.1" 302 0
"GET /accounts/google/login/callback/?state=<some_state>&code=<some_code>&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=0&prompt=consent HTTP/1.1" 302 0

React is expecting a token to ultimately store it in cookies. Previously I had managed to get this working with FastAPI (fastapi-users) and React, where React would GET that last url (the one with the state and code params) and from that response it would extract response.data["access_token"], store it in cookies, and the frontend would now consider itself authenticated.

What is the equivalent here? How can I get that access token now that I'm using django-ninja? Apologies if this is a basic question but I'm pretty lost and other sites are not providing me with answers. Thanks in advance.

Jsalaz1989 avatar Feb 05 '24 17:02 Jsalaz1989

I may have figured it out now (I had spent a couple of days on this before I posted my previous request). Not sure if this is hacky or a bad practice but it seems to work.

Indeed django-ninja has little to do with this process since essentially Django grabs the access token created with django-allauth from the database and stores it in cookies, which React is able to grab. Btw this means I had to switch from ninja.security.django_auth to ninja.security.HttpBearer.

Broadly speaking I have:

  1. Started a new app (eg. /auth).
  2. In /auth/views.py I create this 'intermediate' view:
from django.core.handlers.wsgi import WSGIRequest
from allauth.socialaccount.models import SocialToken
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser

def login_view(request: WSGIRequest):
    user: AbstractBaseUser | AnonymousUser = request.user
    token_info: SocialToken | None = SocialToken.objects.filter(account__user=user, account__provider="google").first()  3 this requires SOCIALACCOUNT_STORE_TOKENS = True in settings.py
    if not token_info:
        return 404, {"message": "User token not found in database"}
    # return JsonResponse({"access_token": token_info.token})    I was hoping something like this could work but I couldn't figure out how to use this from my React endpoint
    response = HttpResponseRedirect("http://localhost:5173/get-token")  # my React endpoint
    response.set_cookie("access_token", token_info.token)
    return response
  1. I add that view to /auth/urls.py:
from django.urls import path
from .views import login_view

urlpatterns = [path("auth/myloginview", login_view),]
  1. And I add that into my root urls.py, ie. path("myauth/", include("auth.urls")).
  2. My React endpoint at /get-token has a useEffect that runs this function:
    const getAccessTokenFromCookies = () => {
        console.log("Getting token from cookies");
        let cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)access_token\s*\=\s*([^;]*).*$)|^.*$/, "$1");
        #console.log(`cookieValue = ${cookieValue}`);
        if (!login) {                      # login() is basically a setState()
            throw Error("login function should not be null");
        }
        login(cookieValue);
    };

That login() function comes from some tutorial I followed which has you create a useToken() and useAuth() hooks / context but that's beyond the scope of this issue and probably varies from your chosen client-side implementation.

Idk if this is super correct but it gets the job done. I'm open to any reasons why this is not a good way of doing things though.

Jsalaz1989 avatar Feb 06 '24 19:02 Jsalaz1989

Hi Everyone. I wrote a quick and dirty social login function that takes advantage of django-allauth functionality. The code basically replicates the functionality of django-rest-auth's SocialLoginSerializer validation. So removes any dependencies on DRF and doesn't require python-social-auth. Tested and working with Google OAuth so far. I'm sure it can be improved upon, but hope it helps other as a starting point.

from allauth.socialaccount.helpers import complete_social_login
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialLogin
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter
from django.contrib.auth import get_user_model
from django.http.request import HttpRequest
from ninja import ModelSchema, Router, Schema

router = Router(tags=["Registration"])


class Error(Schema):
    message: str


class SocialAccountSchema(ModelSchema):
    class Meta:
        model = SocialAccount
        fields = (
            "id",
            "provider",
            "uid",
            "last_login",
            "date_joined",
        )


class SocialLoginSchema(Schema):
    access_token: str


def social_login(
    request,
    app: SocialApp,
    adapter: OAuth2Adapter,
    access_token: str,
    response=None,
    connect=True,
):
    """
    Uses allauth to complete a social login
    If connect is True, then a new social account will be connected to an existing user
    Otherwise, raises an error if the email already exists
    If the email does not exist, then a new user will be created
    Apparently, its not very secure to use connect = True for lesser known social apps
    """
    if not isinstance(request, HttpRequest):
        request = request._request
    token = adapter.parse_token({"access_token": access_token})
    token.app = app
    try:
        response = response or {}
        login: SocialLogin = adapter.complete_login(request, app, token, response)
        login.token = token
        complete_social_login(request, login)
    except Exception as e:
        return 400, {"message": f"Could not complete social login: {e}"}
    if not login.is_existing:
        User = get_user_model()
        user = User.objects.filter(email=login.user.email).first()
        if user:
            if connect:
                login.connect(request, user)
            else:
                return 400, {"errors": ["Email already exists"]}
        else:
            login.lookup()
            login.save(request)
    return 200, login.account


@router.post(
    "/google-login",
    response={200: SocialAccountSchema, 404: Error, 400: Error},
    auth=None,
)
def google_login(request, payload: SocialLoginSchema):
    try:
        app = SocialApp.objects.get(name="Google")
    except SocialApp.DoesNotExist:
        return 404, {"message": "Google app does not exist"}
    adapter = GoogleOAuth2Adapter(request)
    return social_login(request, app, adapter, payload.access_token)

leadrobot avatar Mar 17 '24 17:03 leadrobot

I've explored this issue for a while and concluded that using Firebase Auth is the most robust and straightforward solution. It supports popular providers such as Google, Instagram, Facebook, Apple, etc. Other methods appear more complex to integrate. Unfortunately, I haven't found an easy plug-and-play library for Django Ninja to handle this seamlessly. For those managing a production site, relying on external authentication providers like Auth0 or Firebase Auth seems to be the safest option, based on my research. There might be other options.

vic-cieslak avatar Mar 17 '24 18:03 vic-cieslak

I've explored this issue for a while and concluded that using Firebase Auth is the most robust and straightforward solution. It supports popular providers such as Google, Instagram, Facebook, Apple, etc. Other methods appear more complex to integrate. Unfortunately, I haven't found an easy plug-and-play library for Django Ninja to handle this seamlessly. For those managing a production site, relying on external authentication providers like Auth0 or Firebase Auth seems to be the safest option, based on my research. There might be other options.

https://github.com/pennersr/django-allauth is a very robust and actively maintained solution with 8.8k stars and 597 contributors on Github . The main benefit is that you can register your social apps and manage social accounts in the Django admin. I think people were just struggling to connect their API views to the social login logic that allauth provides.

leadrobot avatar Mar 17 '24 18:03 leadrobot

Yes, I know this one, wish I could use it but integration seems complex to me due to not great knowledge of social auth and JWT which adds complexity as I'm using https://github.com/eadwinCode/django-ninja-jwt

Most django-allauth examples are session based which doesn't help.

vic-cieslak avatar Mar 17 '24 18:03 vic-cieslak

Agreed it seems a bit complex but if you inspect django-rest-auth you will see that its not actually that much code. They handle JWT with all-auth rather gracefully (just remember that most of the logic is contained in the serializers and not the views). Its worth it to avoid the vendor lock in and the future fees that a solution like Firebase entails IMO. Also this is a Django Ninja issue thread so I'm providing a solution that uses Django Ninja.

leadrobot avatar Mar 17 '24 18:03 leadrobot

@leadrobot thanks for the snippet! Are you getting access_token on the frontend? It don't seems secure as it involves using client secret on the frontend.

ivan-halo avatar Mar 19 '24 19:03 ivan-halo

@leadrobot thanks for the snippet! Are you getting access_token on the frontend? It don't seems secure as it involves using client secret on the frontend.

To me, I am using nuxt-auth and it works in server-side. So I guess there is no problem. The only exposing token is JWT (access token) which is from django server.

quroom avatar Mar 20 '24 05:03 quroom

@leadrobot thanks for the snippet! Are you getting access_token on the frontend? It don't seems secure as it involves using client secret on the frontend.

Correct me if I'm wrong but you should only need to use the client ID on the frontend which is not a secret. I haven't got to the front end implementation but I was expecting to have a link like https://accounts.google.com/o/oauth2/v2/auth?client_id=myclientid&redirect_uri=somewhereinmyapp&response_type=token... then the frontend would get the token and hit the google-login endpoint to register/login. I could be way off base here. Please let me know if you see any security issues or I'm making bad assumptions.

leadrobot avatar Mar 25 '24 19:03 leadrobot

As far as django-allauth is concerned, an official API is under development, and the specification (as well as a React example app) are already up for review. Note that this API is framework agnostic, it does not require Ninja nor DRF.

You can read up more here: https://allauth.org/news/2024/04/api-feedback/

pennersr avatar Apr 12 '24 09:04 pennersr