[API][Instagram] Signup / Login / Login Decorator

sjp5554·2020년 5월 23일
0

Takeaways

  • When using postman, always use double quotes
  • When login is successful, always return the token after performing .decode('utf-8') operation. JWT.encode() or decode() function is totally different from regular encode/decode('utf-8') methods that simply change byte type to a string type and vice versa. If neglected, Json byte not serializable error may occur
  • Make sure SECRET_KEY and HASH are imported correctly from settings, not my_settings.py. If neglected, jwt.DecodeError may occur.
  • When filtering query with Q, make sure your boolean logic is correct; if there are null values included in the database (if some fields are set as null=True), all null values can be retrieved and filtering may not be done as expected.
  • When querying, either access by following field_id = integer sytnax, or field = object instance syntax.
Comment.objects.create(feed=Feed.objects.get(
                id=data['feed_id']), account=user, content=data['text'])
// Or
Comment.objects.create(feed=Feed.objects.get(
                id=data['feed_id']), account_id=2, content=data['text'])

Signup

class RegisterView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            e_password = bcrypt.hashpw(
                data['password'].encode('utf-8'), bcrypt.gensalt())
            Account.objects.create(
                email_or_phone=data['email_or_phone'],
                realname=data['realname'],
                username=data['username'],
                password=e_password.decode('utf-8'),
            )
            // if registered successfully
            return JsonResponse({'message': 'Registration Successful!'}, status=200)
        except IntegrityError: // Fields that are unique=True 
            return JsonResponse({'message': 'EXISTING_VALUE'}, status=400)
        except KeyError:
            return JsonResponse({'message': 'INVALID_KEY'}, status=400)

Procedure for Storing User Password

Use bcrypt for Authentication

Unicode-objects must be encoded before hashing
(1) bytes(‘1234’,’utf-8’) >>> b’1234’
(2)
b = ‘1234’
c = b.encode(‘utf-8’) # unicode text representation method
// c.decode('utf-8') will result in a string now

Next,

a = bcrypt.hashpw(c, bcrypt.gensalt())

Or,

a = bcrypt.hashpw(‘1234’.encode(‘utf-8’), bcrypt.gensalt())
>>> b'2b$12FrZe6p0eeCcusxKPQbIrguJxyzZmm5CSBRvLZthyK5uhI.7u5qOae'

Once you hash password with bcrypt, you cannot decode it back to the original String in a comprehensive manner. When you do try to decode it, Python will simply get rid of the 'b' (binary) notation in front of the hashed result and return it:
>>> 2b$12FrZe6p0eeCcusxKPQbIrguJxyzZmm5CSBRvLZthyK5uhI.7u5qOae'

Hence, when a user attempts to login with a password input, you need to encode the input password and compare it with already encoded password stored in your database, rather than the way around.

bcrypt.checkpw(‘1234’.encode(‘utf-8’), a.encode('utf-8'))

The password input to be evaluated does not require gensalt() or hashpw() operations. bcrypt.haspw() returns an encrypted result that contains information about which hashing algorithm is used for encryption, number of key stretches, and salts. bcypt.checkpw() can get hints from this information and accurately evaluate whether the input password is the same as the one in the databse. Moreover, gensalt() utilizes different salts each time, which means that providing gensalt() option to the input password can result in comparison errors.

Because the data type for password field in the database is CharField, the password had been decoded before being saved. Therefore, the password in the database also needs to be encoded before being compared with the input password.

Login

class LoginView(View):
    # post method: user log-in with any one of username, email, or phone
    def post(self, request):
        data = json.loads(request.body)
        try:
            if Account.objects.filter(
                Q(username=data.get('username')) |
                Q(email_or_phone=data.get('email_or_phone'))
            ).exists():
                user = Account.objects.get(
                    Q(username=data.get('username')) |
                    Q(email_or_phone=data.get('email_or_phone'))
                )
                if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
                    # if password is correct
                    token = jwt.encode({'user_id': user.id},
                                       SECRET_KEY, algorithm=HASH)
                    return JsonResponse({'message': 'login successful!', 'token': token.decode('utf-8')}, status=200)
            return JsonResponse({'message': 'Incorrect id or password'}, status=400)
        except KeyError:
            return JsonResponse({'message': 'INVALID_KEY'}, status=400)

Encoding and decoding JWT

Use JWT for authorization

a = jwt.encode({‘user_id’:1}, ‘secret’, algorithm=‘HS256’)
b = jwt.decode(a, ‘secret’, algorithm=‘HS256’)

Unlike bcrypt, JWT can be decrypted. Moreover, only the signature part is encrypted, and header and payload are simply encoded in base64. The signature is encrypted with a hash algorithm of your choice, a SECRET KEY, the contents of the header and the payload.

Login Decorator

Having a login decorator simplicates authorization process. It checks if the user's request contains a valid token, and whether the user is valid and has logged in.

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        try:
            if request.headers.get('Authorization'):
                user_token = request.headers.get('Authorization')
                user_info = jwt.decode(user_token, SECRET_KEY, HASH)
                if Account.objects.filter(pk=user_info['user_id']).exists():
                    user = Account.objects.get(pk=user_info['user_id'])
                    request.user = user  # request 에 존재하지 않던 user 정보를 추가
                    return func(self, request, *args, **kwargs)
                else:
                    // Toekn's user information does not match database information: Account does not exist
                    return JsonResponse({'message': 'No matching account'}, status=401)
            // header is missing token: the user needs to log in first
            return JsonResponse({'message': 'No token'}, status=401)
        // Token's user information is invalid
        except KeyError:
            return JsonResponse({'message': 'INVALID_KEY'}, status=400)
        except jwt.DecodeError:
            return JsonResponse({'message': 'INVALID_TOKEN'}, status=401)
    return wrapper

0개의 댓글