Selamlar bu yazıda Flask ve JWT ile Authentication İşlemleri hakkında bazı bilgiler vereceğim. Bir önceki yazı Caddy Server ve PHP hakkındaydı. Yazıyı yazarken virtualenv kullanacağım. Eğer yok ise onu kurmakla başlayabilirsiniz.
Flask ve JWT ile Authentication İşlemleri
Bu yazıda virtualenv kurulumunu göstereceğim. Virtualenv ile Flask, JWT gibi modülleri geliştirici ortamımıza indireceğiz. Basit olarak bir decorator yazıp her defasında token var mı yok mu kontrolü yapmaktan kurtulacağız. Ayrıca JWT resmi sitesinden ortaya çıkan token geçerli mi değil mi kontrolü yapacağız. Son olarak da bu yazının sonunda, anlatılan bilgilerin aktarıldığı Github repositorysini bulacağız.
Nerede Kullandım?
Bu yazıyı yazmadan önce JWT bir ihtiyaç oldu. Aslında JWT olmadan da giderilebilecek bir ihtiyaç fakat güvenlik gibi nedenlerden ötürü hard data saklamaktan kaçınıyoruz. Vue ya da envai çeşit SPA Framework ile kullanıcıların giriş yapıp data insert edeceği sistemler inşaa edebiliriz. Bunun gibi bir sistemi ben de deniyordum. İyi de session işi nasıl hallolacak konusunda bir hayli takıldım. Araştırınca JWT üzerine gidildiğini gördüm. Bir diğeri de Okta bu arada.
Virtualenv İle Geliştirme Ortamımızı Hazırlayalım
Henüz bilinmiyorsa söyleyeyim, virtualenv dediğimiz şey izole şekilde Python geliştirme ortamı oluşturan bir tool. Her proje kendi ekstra modüllerine sahip olur. Bu modüller sadece o proje kapsamında kullanılır. Yani bu sistemi bir yere taşıdığınızda tekrar tekrar o modülleri sisteme kurmanız gerekmez zaten bunları freeze edersiniz. Eğer yok ise önce virtualenv kuralım:
pip install virtualenv
Ardından geliştirme ortamımızı ayarlayalım. Ben şu anda ~/Projects/py klasörü altında bulunuyorum. Şu komutları girelim:
mkdir jwt_app cd jwt_app virtualenv venv . venv/bin/activate
Eğer işlem başarılıysa konsol ekranı şöyle görüntülenecek:
(venv) ~/Projects/py/jwt_app
Şu anda bize lazım olan şey Flask ve PyJWT modüllerinin kurulumu. Konsolu kapatmadan şu komutları girelim:
pip install Flask pip install PyJWT
Bu komutlarla JWT ve Flask kurulumunu gerçekleştirdik. Hemen bir adet main.py dosyasını bu dizine açalım. Eğer VSCode gibi editörler kullanıyorsanız entegre terminalde virtualenv ile çalışabilirsiniz. Basit bir Flask yapısı şöyle olsun:
from flask import Flask, jsonify app = Flask(__name__) @app.route('/api/v1/user/<id>', methods=['GET']) def users(id): return "User Id: {}".format(id) if __name__ == '__main__': app.run(port=2121, debug=True)
Kısacası şöyle bir istekte bulunmuşuz gibi olacak: http://127.0.0.1:2121/api/v1/user/2 Ancak bu istekte bir SPA tarafında session problemi yaşamamız olası. Şimdi kullanıcı girişi ve kullanıcı kaydı için iki adet route oluşturacağız. Senaryo şu, kayıtlı kullanıcı id’si verilen bir başka kullanıcıyı görebilir. Bunun için de JWT tarafında valide edilmiş bir anahtara ihtiyacımız var. Önce rotalarımızı yazalım. Senaryoya göre kullanıcı kayıt olacak giriş yapacak. Bizim için kayıttan ziyade giriş önemli.
@app.route('/api/v1/register', methods=['POST']) def register(): username = request.form.get('username') password = request.form.get('password') return jsonify({'message': 'You registered!'}) @app.route('/api/v1/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') token = '' if username == 'admin' and password == 'admin': token = "Hash Burada"
Şimdi login işleminde eğer veri tabanında yaptığımız sorgunun sonucu doğru dönüyorsa hash oluşturalım. Bu hash değerinde normalde parola vs. saklamayın ama ben örnek data olarak bunları döndüreceğim. Bu hash için flask özelinde bir secret key belirtelim ve ayrıca datetime modülünü içeriye aktaralım. Dosyanın en üstüne
import datetime
ekleyelim ve ardından app değişkeninin hemen altına secret key tanımlayalım. Kısacası üst kısım şöyle olacak:
import datetime app = Flask(__name__) app.config['SECRET_KEY'] = 'GIZLI ANAHTAR CIA BIZI IZLIYOR'
Son haliyle bu işlemlerden sonra login işlemini yaptırdığımız metodumuz şeklini aldı:
user ve password değerleri bizim özelimizde bir değer iken, exp değeri “expiration time” yani tokenin sona eriş tarihini veriyor. JWT ile bazı claimler kullanabilirsiniz. (Buradan bakınız). Bu tarih nereden geliyor? Şu anki zamanımızı alıyor ve bu zamanın üzerine 30 dakika ekliyor. Bu kodu şöyle de yapabilirdik:
datetime.datetime.utcnow() + datetime.timedelta(hours=24)
Yukarıdaki kod token 24 saat geçerli olacaksa kullanılabilir. Ancak şu an örneğimiz 30 dakika üzerinden ilerliyor. Burada jwt’ye ait encode metodu, ilk parametreyi obje türünden bir payload şeklinde alırken, ikinci parametresi secret key yani çok gizli anahtarımız oluyor. Bu anahtarı zaten ön yüzde saklamak pek mantıklı değil de oldu da backend projenizi git gibi public bir ortama attınız işte o zaman sorun oluyor. O yüzden .env gibi çözümler ideal oluyor.
@app.route('/api/v1/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') token = '' if username == 'admin' and password == 'admin': token = jwt.encode({ 'user': username, 'password': password, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30) }, app.config['SECRET_KEY']) else: token = 'Invalid username or password' return jsonify({'token': token})
Ardından basit bir login isteğinde bulunalım:
Çıkan Token İçin Doğrulama İşlemi
Çıkan token tarafında doğrulama işlemini jwt.io üzerinden gerçekleştireceğiz.
Çıkan token üç parçalı base64 değerden oluşuyor. Algoritmayı, payloadı ve secret key’in şifrelenmiş halini içermekte. Özellikle bize ait password ve username değeri burada ortadaki şifreli değerde saklı. Bunu herhangi bir base64 decode sitesinde hemen çözersiniz. Eğer bu expiration time değeri 30 saniye olsaydı bu imza kullanılamayacaktı.
Şimdi az önceki user id endpointine geri dönelim. Metodumuzu şöyle değiştirelim:
@app.route('/api/v1/user/<token>/<id>', methods=['GET']) def users(token, id): return "Token: {} User Id: {}".format(token, id)
Postman ile isteğimizi şöyle gerçekleştirelim:
Alınan Token Değerinin Decode Edilmesi
Bu değerin geçerli bir değer olup olmadığını decode ederek görelim. Yine bu istekte bulunduğumuz metod içinde bazı değişiklikler gerçekleştireceğiz:
@app.route('/api/v1/user/<token>/<id>', methods=['GET']) def users(token, id): decoded = jwt.decode(token, app.config['SECRET_KEY']) return jsonify({'decoded': decoded})
Şu anda decode işlemini gerçekleştirdik. Bu istekte yolladığım token 5 saniyelik bir token değerine sahipti. Bu yüzden bir hata aldım. O hata şuna benziyor:
raise ExpiredSignatureError('Signature has expired') ExpiredSignatureError: Signature has expired
Buradan da anlayacağımız üzere token geçerliliğini kaybetmiş durumda. Gidip 30 dakikalık bir tane alayım hemen 🙂 Yeni çıkan 30 dakikalık token ile birlikte istek yolladığımızda çıkan dönen json değer bir hayli mutlu etti beni:
{ "decoded": { "exp": 1515869548, "password": "admin", "user": "admin" } }
Şu anda başarılı bir şekilde token tabanlı doğrulamayı yaptık diyelim. Örneğin angular projesinde ya da vue projesinde bu token’i localStorage gibi bir ortamda saklarsınız. Oradan alır ve endpointe bu tokeni basarsınız. Sistem doğru çalışıyor tamam da her yazdığımız route için if token ise falan kontrolünü yapmayalım bi zahmet. Gidip bunun için bi decorator yazalım işimizi de kolaylaştırmış olur. Ben şöyle bir decorator yazdım.
Yetkilendirme İçin Wrapper Yazalım
def auth(f): @wraps(f) def wrapper(*args, **kwargs): token = kwargs['token'] if not token: return jsonify({'message': 'Token required!'}), 403 try: data = jwt.decode(token, app.config['SECRET_KEY']) except: return jsonify({'message': 'Token expired!'}), 403 return f(*args,**kwargs) return wrapper
Kullanıcı ile etkileşime geçen her route mutlaka token değerine sahip olmalı. Eğer token zorunlu değilse belirtmezsiniz. Yazdığımız decorator ve route metodunda kullanımı şöyle olacak:
from functools import wraps app = Flask(__name__) app.config['SECRET_KEY'] = 'GIZLI ANAHTAR CIA BIZI IZLIYOR' def auth(f): @wraps(f) def wrapper(*args, **kwargs): try: token = kwargs['token'] except: token = request.args.get('token') or \ request.form.get('token') if not token: return jsonify({'message': 'Token required!'}), 403 try: data = jwt.decode(token, app.config['SECRET_KEY']) except: return jsonify({'message': 'Token invalid!'}), 403 return f(*args,**kwargs) return wrapper @app.route('/api/v1/user/<token>/<id>', methods=['GET']) @auth def users(token, id): message = 'Token is validate' return jsonify({'message': message})
Yukarıdaki auth decoratorunde token route decoratörüne geçirilmiş olabilir ya da query stringden gelmiş olabilir ya da form post edildiği anda gelmiş olabilir. Bu durumları düşünerek böyle bir kontrol sağladık. Siz bunu daha ileri şekilde kullanabilirsiniz. Bunun doğrulamak adına bir adet post değeri içeren method da yazalım:
@app.route('/api/v1/user/update', methods=['POST']) @auth def update_user(): token = request.form.get('token') user_id = request.form.get('id') return token
Evet token doğru olduğu için, bu metod ne istediysek onu döndürdü. Son durumda yazdığımız kodlar şu şekilde:
from flask import Flask, jsonify, request import jwt import datetime from functools import wraps app = Flask(__name__) app.config['SECRET_KEY'] = 'GIZLI ANAHTAR CIA BIZI IZLIYOR' def auth(f): @wraps(f) def wrapper(*args, **kwargs): try: token = kwargs['token'] except: token = request.args.get('token') or \ request.form.get('token') if not token: return jsonify({'message': 'Token required!'}), 403 try: data = jwt.decode(token, app.config['SECRET_KEY']) except: return jsonify({'message': 'Token invalid!'}), 403 return f(*args,**kwargs) return wrapper @app.route('/api/v1/user/<token>/<id>', methods=['GET']) @auth def users(token, id): message = 'Token is validate' return jsonify({'message': message}) @app.route('/api/v1/user/update', methods=['POST']) @auth def update_user(): token = request.form.get('token') user_id = request.form.get('id') return token @app.route('/api/v1/register', methods=['POST']) def register(): username = request.form.get('username') password = request.form.get('password') return jsonify({'message': 'You registered!'}) @app.route('/api/v1/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') token = '' if username == 'admin' and password == 'admin': token = jwt.encode({ 'user': username, 'password': password, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30) }, app.config['SECRET_KEY']) else: token = 'Invalid username or password' return jsonify({'token': token.decode('UTF-8')}) if __name__ == '__main__': app.run(port=2121, debug=True)
Nasıl Kullanırım?
Yukarıda başlarda belirttiğim üzere isterseniz bunu SPA Frameworkler ile gerçekleştirdiğiniz uygulamalarda kullanabilirsiniz. Yani localStorage tarafında tutacağınız, expiration time değeri makul olan token her route’a gönderilir. Eğer token invalidate yani geçersiz ise kullanıcının bulunduğu sayfayı login’e yönlendirirsiniz.
Bu payload üzerinde user id, username, name, surname gibi basit değerleri tutmak isteyebilirsiniz. Ancak bir parola tutmak! Asla 🙂 Sonuçta bu token veri tabanı sorgusundan geçtikten sonra üretilip kullanıcıya sunuluyor yani kullanıcı zaten giriş yapmış demektir.
Söz verdiğim gibi uygulamanın Github linki:
https://github.com/aligoren/flask-jwt-app
Yararlı Linkler
Bu kısımda JWT ve Python ile çalışırken işinize yarayacak linkler yer alıyor:
Yazının sonuna geldik arkadaşlar. Yazı biraz uzun oldu. Bunun için öncelikle affınıza sığınıyorum.