Saleor: How to login with social media for Saleor

Created on 27 Dec 2019  Â·  4Comments  Â·  Source: mirumee/saleor

What I'm trying to achieve

… Login saleor with other social media

Describe a proposed solution

... social-auth-app-django
I've gone through the code base and found that social-auth-app-django and relevant stuff were gradually phased out
Why is that? Could we re-enable it?

By the way, detail setup, step by step document will be better (Reading the doc front and back, found no detail setup tutorials)

Most helpful comment

I'm using social-auth-app-django and it's quite easy to setup, however, OAuth-flow doesn't easy to work with stateless graphql in my opinion because it's designed to work with session for the WEB.
https://github.com/python-social-auth/social-app-django/blob/b5a6434cb7dd308debc694f94753398a15445580/social_django/views.py#L103

For a scenario that mobile applications will use an SDK to signup a user within the app, I'd tried integrate social-auth-app-django into saleor's api. Here is the code.

import graphene
from functools import wraps
from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _

from promise import Promise, is_thenable
from django.dispatch import Signal
token_issued = Signal(providing_args=['request', 'user'])

from graphql_jwt.exceptions import JSONWebTokenError, PermissionDenied
from graphql_jwt.mixins import ResolveMixin, ObtainJSONWebTokenMixin
from graphql_jwt.decorators import setup_jwt_cookie
from graphql_jwt.settings import jwt_settings
from graphql_jwt.shortcuts import get_token
from graphql_jwt.refresh_token.shortcuts import refresh_token_lazy
from social_django.utils import load_strategy, load_backend
from social_django.compat import reverse


from ..account.types import User
from ..core.types import Error

def token_auth(f):
    @wraps(f)
    @setup_jwt_cookie
    def wrapper(cls, root, info, **kwargs):
        context = info.context
        context._jwt_token_auth = True

        def on_resolve(values):
            user, payload = values
            payload.token = get_token(user, context)

            if jwt_settings.JWT_LONG_RUNNING_REFRESH_TOKEN:
                payload.refresh_token = refresh_token_lazy(user)

            return payload

        token = kwargs.get('access_token')
        backend = kwargs.get('backend')
        context.social_strategy = load_strategy(context)
        # backward compatibility in attribute name, only if not already
        # defined
        if not hasattr(context, 'strategy'):
            context.strategy = context.social_strategy
        uri = reverse('social:complete', args=(backend,))
        context.backend = load_backend(context.social_strategy, backend, uri)

        user = context.backend.do_auth(token)

        if user is None:
            raise JSONWebTokenError(
                _('Please, enter valid credentials'))

        if hasattr(context, 'user'):
            context.user = user

        result = f(cls, root, info, **kwargs)
        values = (user, result)

        token_issued.send(sender=cls, request=context, user=user)

        if is_thenable(result):
            return Promise.resolve(values).then(on_resolve)
        return on_resolve(values)
    return wrapper


class JSONWebTokenMutation(ObtainJSONWebTokenMixin, graphene.Mutation):
    class Meta:
        abstract = True

    @classmethod
    @token_auth
    def mutate(cls, root, info, **kwargs):
        return cls.resolve(root, info, **kwargs)


class CreateOAuthToken(ResolveMixin, JSONWebTokenMutation):
    errors = graphene.List(Error, required=True)
    user = graphene.Field(User)

    class Arguments:
        access_token = graphene.String(description="Access token.")
        backend = graphene.String(description="Authenticate backend")

    @classmethod
    def mutate(cls, root, info, **kwargs):
        try:
            result = super().mutate(root, info, **kwargs)
        except JSONWebTokenError as e:
            return CreateOAuthToken(errors=[Error(message=str(e))])
        else:
            return result

    @classmethod
    def resolve(cls, root, info, **kwargs):
        return cls(user=info.context.user, errors=[])

class OAuthMutations(graphene.ObjectType):
    oauth_token_create = CreateOAuthToken.Field()

And schema.graphql

type Mutation {
  ...
  oauthTokenCreate(backend: String!, access_token: String!): CreateOAuthToken
  ...
}

settings.py

INSTALLED_APPS = [
  "social_django",
  ...
]

AUTHENTICATION_BACKENDS = [
    "social_core.backends.google.GoogleOAuth2", # <-
    "graphql_jwt.backends.JSONWebTokenBackend",
    "django.contrib.auth.backends.ModelBackend",
]

# social-auth
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_URL_NAMESPACE = 'social'
SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'social_core.pipeline.user.create_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
    'social_core.pipeline.social_auth.associate_by_email',
)

SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'INSERT_PROVIDED_KEY_HERE'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'INSERT_PROVIDED_SECRET_HERE'

Solution for SPA

All 4 comments

I'm using social-auth-app-django and it's quite easy to setup, however, OAuth-flow doesn't easy to work with stateless graphql in my opinion because it's designed to work with session for the WEB.
https://github.com/python-social-auth/social-app-django/blob/b5a6434cb7dd308debc694f94753398a15445580/social_django/views.py#L103

For a scenario that mobile applications will use an SDK to signup a user within the app, I'd tried integrate social-auth-app-django into saleor's api. Here is the code.

import graphene
from functools import wraps
from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _

from promise import Promise, is_thenable
from django.dispatch import Signal
token_issued = Signal(providing_args=['request', 'user'])

from graphql_jwt.exceptions import JSONWebTokenError, PermissionDenied
from graphql_jwt.mixins import ResolveMixin, ObtainJSONWebTokenMixin
from graphql_jwt.decorators import setup_jwt_cookie
from graphql_jwt.settings import jwt_settings
from graphql_jwt.shortcuts import get_token
from graphql_jwt.refresh_token.shortcuts import refresh_token_lazy
from social_django.utils import load_strategy, load_backend
from social_django.compat import reverse


from ..account.types import User
from ..core.types import Error

def token_auth(f):
    @wraps(f)
    @setup_jwt_cookie
    def wrapper(cls, root, info, **kwargs):
        context = info.context
        context._jwt_token_auth = True

        def on_resolve(values):
            user, payload = values
            payload.token = get_token(user, context)

            if jwt_settings.JWT_LONG_RUNNING_REFRESH_TOKEN:
                payload.refresh_token = refresh_token_lazy(user)

            return payload

        token = kwargs.get('access_token')
        backend = kwargs.get('backend')
        context.social_strategy = load_strategy(context)
        # backward compatibility in attribute name, only if not already
        # defined
        if not hasattr(context, 'strategy'):
            context.strategy = context.social_strategy
        uri = reverse('social:complete', args=(backend,))
        context.backend = load_backend(context.social_strategy, backend, uri)

        user = context.backend.do_auth(token)

        if user is None:
            raise JSONWebTokenError(
                _('Please, enter valid credentials'))

        if hasattr(context, 'user'):
            context.user = user

        result = f(cls, root, info, **kwargs)
        values = (user, result)

        token_issued.send(sender=cls, request=context, user=user)

        if is_thenable(result):
            return Promise.resolve(values).then(on_resolve)
        return on_resolve(values)
    return wrapper


class JSONWebTokenMutation(ObtainJSONWebTokenMixin, graphene.Mutation):
    class Meta:
        abstract = True

    @classmethod
    @token_auth
    def mutate(cls, root, info, **kwargs):
        return cls.resolve(root, info, **kwargs)


class CreateOAuthToken(ResolveMixin, JSONWebTokenMutation):
    errors = graphene.List(Error, required=True)
    user = graphene.Field(User)

    class Arguments:
        access_token = graphene.String(description="Access token.")
        backend = graphene.String(description="Authenticate backend")

    @classmethod
    def mutate(cls, root, info, **kwargs):
        try:
            result = super().mutate(root, info, **kwargs)
        except JSONWebTokenError as e:
            return CreateOAuthToken(errors=[Error(message=str(e))])
        else:
            return result

    @classmethod
    def resolve(cls, root, info, **kwargs):
        return cls(user=info.context.user, errors=[])

class OAuthMutations(graphene.ObjectType):
    oauth_token_create = CreateOAuthToken.Field()

And schema.graphql

type Mutation {
  ...
  oauthTokenCreate(backend: String!, access_token: String!): CreateOAuthToken
  ...
}

settings.py

INSTALLED_APPS = [
  "social_django",
  ...
]

AUTHENTICATION_BACKENDS = [
    "social_core.backends.google.GoogleOAuth2", # <-
    "graphql_jwt.backends.JSONWebTokenBackend",
    "django.contrib.auth.backends.ModelBackend",
]

# social-auth
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_URL_NAMESPACE = 'social'
SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'social_core.pipeline.user.create_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
    'social_core.pipeline.social_auth.associate_by_email',
)

SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'INSERT_PROVIDED_KEY_HERE'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'INSERT_PROVIDED_SECRET_HERE'

Solution for SPA

@ace-han Hey! Currently, login via social media is not available. @gsoec has provided a good solution how to setup the social media login by yourself. If you have more questions just let me know.

@karolkielecki I think email as username some kind of making it harder to integrate with OAuth login (some would be phone number or just don't have an email field)

I think this feature is needed since most start-ups are not mighty enough to host brand-new email(user) registration

Was this page helpful?
0 / 5 - 0 ratings