swsnu / swppfall2019

31 stars 22 forks source link

[HW3] CSRF 인증 및 Sign in 관련 질문 #145

Open Spiraline opened 5 years ago

Spiraline commented 5 years ago

안녕하세요 조교님 항상 수고하십니다.

HW3을 진행하는 과정에서 인증 관련해서 문제가 생겨서 질문드립니다.

1) signin 관련 view를 작성한 뒤 ARC.pdf에 있는 내용대로 따라했을 때 토큰을 받고 토큰을 이용해 POST /api/signin/ 까지는 성공했으나 이후 POST /api/aritlces의 헤더에 해당 sessionid를 넣고 실행을 했을 때에는 CSRF 인증 오류 (403)가 발생합니다. 이 때의 CSRF token은 다시 GET /api/token/을 통해서 받아온 token이어야 하나요? 아니면 POST /api/aritlce/ 의 response에서도 CSRF token이 존재하는데 이 토큰을 사용해야 하나요?

2) 1번의 두 방법 모두 signin까지는 response status 204를 받으며 성공적으로 기능을 했으나 POST /api/aritlce/ 에서 401 Response를 보였는데요. post에서 request.user를 출력해보니 AnonymousUser가 출력되는 것을 확인할 수 있었습니다. 따라서 로그인이 제대로 되지 않았다고 판단했는데요. views.py 내의 signin 함수는 link를 참고하여 아래와 같이 구현했습니다.

def signin(request):
    if request.method == 'POST':
        try:
            body = request.body.decode()
            user_id = json.loads(body)['username']
            user_pw = json.loads(body)['password']
            user = authenticate(request, username = user_id, password = user_pw)
            if user is not None:
                login(request, user)
                return HttpResponse(status=204)
            else:
                return HttpResponse(status=401)
        except (KeyError, JSONDecodeError) as e:
            return HttpResponse(status=400)
    else:
        return HttpResponse(status=405)

signin까지는 성공했으나 이후 다른 api를 실행할 때에 request.user가 AnonymousUser가 뜨는 것은 어느 방향에서 코드를 수정해야 할까요?

3)

    def setUp(self):
        User.objects.create_user('user1', 'pw1')

    def test_signin(self):
        client = Client(enforce_csrf_checks=True)
        response = client.get('/api/token/')
        csrftoken = response.cookies['csrftoken'].value
        response = client.post('/api/signin/', json.dumps({'username': 'user1', 'password': 'pw1'}),
                               content_type='application/json', HTTP_X_CSRFTOKEN=csrftoken)

        self.assertEqual(response.status_code, 204)

해당 signin에 대해서 위의 테스트 코드를 작성해보았으나 이 역시 401 Response를 보이며 test가 fail되는 것을 확인할 수 있었습니다. 2번에서 언급한대로 ARC를 이용한 POST /api/signin/ 는 성공했으나 테스트 코드는 통과하지 못한 것이 의아했는데 혹시 2번에서의 signin 이후에도 request.user가 AnonymousUser로 출력되는 것과 연관이 있을까요?

감사합니다.

ktaebum commented 5 years ago

1 로그인을 하고 response를 확인해보면 session_id와 항께 csrf token이 새로 나오는데 그 새로운 토큰을 쓰시면 됩니다

2, 3. 네 일단 3번의 문제는 AnonymousUser 의 문제로 인한 것 같구요 일단 signin 함수 자체의 문제는 없어 보여서 (뭐 그래도 모르니 계속 확인은 부탁드립니다)

다른 view에서 request 보냈을 때 user가 제대로 있는지를 계속 확인해보셔야 할 것 같습니다

저게 anonymousUser로 되는 이유는

auth 관련 middleware가 settings.py'django.contrib.auth.middleware.AuthenticationMiddleware',

가 있는데

이 코드가

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
        request.user = SimpleLazyObject(lambda: get_user(request))

에서 request의 user attribute를 get_user의 결과값으로 넣어주거든요

get_user 함수를 보시면

def get_user(request):
    """
    Return the user model instance associated with the given request session.
    If no user is retrieved, return an instance of `AnonymousUser`.
    """
    from .models import AnonymousUser
    user = None
    try:
        user_id = _get_user_session_key(request)
        backend_path = request.session[BACKEND_SESSION_KEY]
    except KeyError:
        pass
    else:
        if backend_path in settings.AUTHENTICATION_BACKENDS:
            backend = load_backend(backend_path)
            user = backend.get_user(user_id)
            # Verify the session
            if hasattr(user, 'get_session_auth_hash'):
                session_hash = request.session.get(HASH_SESSION_KEY)
                session_hash_verified = session_hash and constant_time_compare(
                    session_hash,
                    user.get_session_auth_hash()
                )
                if not session_hash_verified:
                    request.session.flush()
                    user = None

    return user or AnonymousUser()

여기서 user가 None이면 AnonymousUser()를 리턴하게 되어 있는데 session hash verified가 안 되어서 None이 되는 경우가 있어서 이와 관련하여 ARC에서 문제가 있을수도 있다고 생각이 됩니다.

3번 같은 경우에는 사실 한 가지 팁을 드리면 token을 가져오는 test를 따로 두시고 test client에서는 csrf_checkFalse로 두시고 하는 방법이 있습니다.

kyunggeunlee commented 5 years ago

참고로 말씀드리면, session id를 헤더에 넣을 때 반드시 세미콜론(;)을 붙여 주셔야 됩니다. 이것 때문일 수 있으니 한 번 확인해 보세요. image

Spiraline commented 5 years ago

2번의 문제의 경우에는 @kyunggeun-lee 말씀대로 세미콜론을 붙이니 request.user가 user의 이름으로 출력되는 것을 확인하였습니다! 3번의 test의 경우 csrf_check를 False로 해놔도 authenticate에서 None을 return하여 확인해보니

User.objects.create_user('user1', 'email', 'pw1')

와 같이 create_user에서 email parameter를 넣어야 한다는 것을 확인했고 해당 방법으로 해결하였습니다! 그런데 그렇다면 views.py의 스켈레톤에서

def signup(request):
    if request.method == 'POST':
        req_data = json.loads(request.body.decode())
        username = req_data['username']
        password = req_data['password']
        User.objects.create_user(username, password)
        return HttpResponse(status=201)
    else:
        return HttpResponse(status=405)

User.objects.create_user(username, password) 에 이메일에 해당하는 argument가 추가되어야 할 듯 한데 제가 이해한 바가 맞을까요?

두 분 모두 답변 너무 감사드립니다!

ktaebum commented 5 years ago

음 우선 email field가 optional인데 그걸 했다고 되었다니 신기하네요 https://docs.djangoproject.com/en/2.2/ref/contrib/auth/#django.contrib.auth.models.User.email 일단은 email 없이도 되는 게 맞아서 한 번 다시 확인해 보시는 걸 추천드리겠습니다

lllY2Klll commented 5 years ago

User.objects.create_user('user1', 'pw1') 를 하게 되면 인자 전달 순서때문에 'pw1'user.email로 가는 것 같은데 User.objects.create_user(username='user1', password='pw1')처럼 필드를 명확하게 주면 이메일이 없어도 정상적으로 생성되는것으로 보입니다!

Spiraline commented 5 years ago

@lllY2Klll 그런 이유였군요 문서를 읽어보니

create_user(username, email=None, password=None, **extra_fields)

인 것을 보니 email과 password가 모두 optional인 듯 하여 말씀하신대로 두 개만 쓴다면 password가 email에 저장되는 것 같습니다! 문제는 해결이 되었으나 이유에 대해 궁금했는데 알게 되었네요 감사합니다!