【Python Social Auth】PythonSocialAuth - Pipelineの使い方がいまいちわからなかったので真剣にソース確認してみた【Django】

# Social Core の user.py, social_auth.py の仕様について考察

django REST framework × React で、Pyhton Social Authを用いてSocial Login機能を作成したが、
ログイン機能と新規登録機能を分けるためには
デフォルトで有効になっている`social_core.pipeline.user.create_user`を無効化しなければらならい
(※有効のままだと、ログインボタンでも新規登録できてしまう)

ただし、無効化すると認証が行えなくなり、
convert-token 実行できない

なので、user.py, social_auth.py の仕様を理解して、
カスタムユーザでも利用できるようにしたい

## pipelineの確認

まず、pipelineの仕様について

↓はDjangoのsettings.pyに追記することでオーバーライドできるpipelineのデフォルトのリスト

```py
SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.auth_allowed',
    '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',
)
```

各関数の詳細はこちらにまとめた↓
[python social auth pipeline メソッド内容一覧](https://qiita.com/c0ba1t_coke/items/1df141bc22363ff37c4a)

## create_userをコメントアウトしたときの挙動

ドキュメントによると、ユーザ登録を行わず、ログインのみのpipelineにするには
get_username と create_user をコメントアウトすればいいという記載がある

これをコメントアウトした状態でログイン(DRFのconvert-token)しようとすると、
↓のエラーが出て失敗する

```
  File "/home/coke/google-oauth-sample/tutorial/lib64/python3.6/site-packages/drf_social_oauth2/oauth2_grants.py", line 113, in validate_token_request
    'Invalid credentials given.', request=request
oauthlib.oauth2.rfc6749.errors.InvalidGrantError: (invalid_grant) Invalid credentials given. <oauthlib.Request SANITIZED>
```

エラーで出てきた`drf_social_oauth2/oauth2_grants.py`の当該箇所を見てみると

```py
if not user:
    raise errors.InvalidGrantError(
        'Invalid credentials given.', request=request
    )
```
validate_token_request(self, request)関数で、
**userオブジェクトが存在しない場合、このエラーが出ている**ことが分かった

つまり、create_userをコメントアウトしたことで、userオブジェクトが引数からなくなって、
関数が実行できない状態となっている

確かに、user.py の create_user関数を見ても、
返り値に`user`が設定されていることが分かる

```py
def create_user(strategy, details, backend, user=None, *args, **kwargs):
    if user:
        return {'is_new': False}

    fields = {name: kwargs.get(name, details.get(name))
                  for name in backend.setting('USER_FIELDS', USER_FIELDS)}
    if not fields:
        return

    return {
        'is_new': True,
        'user': strategy.create_user(**fields)
    }

```

### ここまでのまとめ

- create_user がある場合 : 返り値に`user`が含まれているため、`validate_token_request`が正常に動作する
- create_user がない場合 : 返り値に`user`が含まれなくなるので、`validate_token_request`がエラーを出力する

## pipeline上で、create_user以外で`user`を返す関数がないか確認する

すべてのメソッドを確認したが、返り値にuserを返すものはほかにはなかった
各メソッドの内容は↓にまとめた

- social_user
- associate_user
- associate_by_email(デフォルトではオフ)

これらは返り値に`user`を返す部分があったが、
`if user:`で始まる条件の時のみであったので、無効

これに関してはドキュメントに記載があった よくわからなかったから無視してた

> Note that this assumes the user is already authenticated, and thus the user key in the dict is populated. In cases where the authentication is purely external, a pipeline method must be provided that populates the user key. Example:

> しかしながらこの設定は、ユーザがすでに認証されていることを想定していることに注意
> つまり、dict内に`user`キーがすでに追加されている状態である
> 認証機能を完全に外部にゆだねる場合、`user`キーを追加するメソッドをパイプラインに含めるようにしなければならない
> 例えば、以下のように`user`をdictに追加するようなオリジナルのメソッドを追加する

# 考察結果

上記の考察から、分かったこと

- デフォルトのcreate_userメソッドをpipelineから外す場合、pipelineメソッドの引数である**kwargs(dict型の変数)内に、`user`オブジェクトを必ず含める必要がある

- pipelineでcreate_userをコメントアウトした場合
  - ほかのどの関数も`user`を返すことはないので、drf_social_oauth2の`validate_token_request`関数で必ずエラーを起こす
  - social側でuserが作成されてないので、無理やりdjangoでuserを作成しても情報が一致せず、pipelineがうまく動かない
    - なぜかuser_detailsが無いといわれる

- create_userをコメントアウトしなかった場合
  - userオブジェクトが引数に追加されるため、初回実行以降は認証はうまくいくようになる

# 回避策

## 方法1. オリジナルのcreate_userを作成してpipelineに組み込む

socialのstorageにユーザ情報が保存されていないためにuserオブジェクトが返されないことがすべての原因なので、
create_userをオリジナルで作る

この場合、結局create_userを作るので、ログインの時と新規作成の場合で処理を分けるようなメソッドにする必要がある
    →不可能？

## 方法2. userオブジェクトを返すオリジナルメソッドをpipelineに組み込む

ユーザの作成はDjangoのviewで行い、ログイン処理はDjangoのDBを参照して、userオブジェクトを返すようにする

```py:users/pipeline.py
from .models import CustomUser

def login_user(response, user=None, *args, **kwargs):

    if not user:
        email = response['email']
        username = response['name']
        login_user = CustomUser.objects.get(username=username, email=email)
        return {
            'is_new': False,
            'user': login_user
        }
```

**この方法で成功!**

usersというapp内にpipeline.pyというファイルを作成

`login_user = CustomUser(ユーザモデル).objects.get(username=username, email=email)`
として、return に {'user': login_user} で渡してあげると、うまく動くようになった

# 残りの課題

未登録の状態でログインボタンを押したときに「そのユーザは登録されていません」のエラーを出すには、
やはり自前でGoogleログイン処理の前に何かユーザ登録の済/未済を判定するエンドポイントを用意する必要がありそう