vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
6.9k stars 418 forks source link

How do I perform google social authentication? #479

Open Jorncg opened 2 years ago

Jorncg commented 2 years ago

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?

alirezapla commented 2 years ago

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

quroom commented 2 years ago

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/

yleclanche commented 12 months ago

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

quroom commented 12 months ago

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

This can be helpful.

Nils3311 commented 11 months ago

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

vitalik commented 11 months ago

@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/

yleclanche commented 11 months ago

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())

quroom commented 10 months ago

@yleclanche Thanks for sharing.

vic-cieslak commented 7 months ago

looking to implement allauth with JWT

Jsalaz1989 commented 7 months ago

@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 commented 7 months ago

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(accountuser=user, accountprovider="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
3. I add that view to /auth/urls.py:

from django.urls import path from .views import login_view

urlpatterns = [path("auth/myloginview", login_view),]


4. And I add that into my root urls.py, ie.  `path("myauth/", include("auth.urls"))`.
5. 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.
leadrobot commented 5 months ago

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)
vic-cieslak commented 5 months ago

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.

leadrobot commented 5 months ago

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.

vic-cieslak commented 5 months ago

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.

leadrobot commented 5 months ago

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.

ivan-halo commented 5 months ago

@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.

quroom commented 5 months ago

@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.

leadrobot commented 5 months ago

@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.

pennersr commented 4 months ago

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/

marin117 commented 3 months ago

After a long time lurking on this topic, I've implemented Google Authentication via custom implementation following Google docs. I've just found out that for me it is the simplest and most RESTful solution. It is not that hard, so I would maybe advise that as an option. After you create a user you can use whatever authentication you want in your app.

https://googleapis.dev/python/google-auth/latest/reference/google.oauth2.html

leadrobot commented 2 months ago

Thanks to the incredible work of @pennersr with allauth headless, Using ninja_jwt with allauth is as simple as...

from allauth.headless.tokens.sessions import SessionTokenStrategy
from django.http import HttpRequest
from ninja_jwt.tokens import SlidingToken

class TokenStrategy(SessionTokenStrategy):
    def create_access_token(self, request: HttpRequest) -> str | None:
        user = request.user
        if user.is_authenticated:
            return str(SlidingToken.for_user(user))
        return None

Setup allauth and headless as per https://docs.allauth.org/en/latest/headless/index.html. Point HEADLESS_TOKEN_STRATEGY to above in settings and off you go.

Jsalaz1989 commented 2 months ago

Thanks to the incredible work of @pennersr with allauth headless, Using ninja_jwt with allauth is as simple as...

from allauth.headless.tokens.sessions import SessionTokenStrategy
from django.http import HttpRequest
from ninja_jwt.tokens import SlidingToken

class TokenStrategy(SessionTokenStrategy):
    def create_access_token(self, request: HttpRequest) -> str | None:
        user = request.user
        if user.is_authenticated:
            return str(SlidingToken.for_user(user))
        return None

Setup allauth and headless as per https://docs.allauth.org/en/latest/headless/index.html. Point HEADLESS_TOKEN_STRATEGY to above in settings and off you go.

@leadrobot Thanks for the example but, just to confirm, does this work with Google oauth? It seems in your example you are having django-ninja create an access token, but doesn't Google provide the access token for you?

marin117 commented 2 months ago

@Jsalaz1989 It really depends of your application and your needs. Google will provide you an access token and you can use that token for user authentication, but if you want to handle authentication by yourself inside your app, you will need to handle user storing inside database and issuing tokens from your application. Google access tokens are used for getting information and access to Google services, but they may not be used for your application where you'll maybe need some sort of token issuing and handling.