Inloggning TDDD80 Mobila och sociala applikationer
11 FEBRUARY 2018 Två deluppgifter 1. Spara lösenord på ett säkert sätt Statisk, långtidslagring Databas på server-sidan 2. Hålla koll på inloggningsstatus Delegeras till varje klient Varför? Komplext (bl.a. synkroniseringsproblem) Svårt att skala upp till många samtidiga användare
11 FEBRUARY 2018 Ett par nya views @app.route('/register', methods=['post']) def register_user(): # lägg in email, lösenord i DB @app.route('/login', methods=['post']) def login_user(): # kolla om lösenord matchar
User User-tabell utökas med några kolumner class User(db.Model): tablename = 'users' id = db.column(db.integer, primary_key=true) name = db.column(db.string(5)) email = db.column(db.string(20), unique=true) passw_hash = db.column(db.string(100))
Viktigt att lösenordet skyddas Vill transformera lösenordet Ska inte kunna reverseras av utomstående Argon2, bcrypt, scrypt, PBKDF2 Lättläst intro: https://code.tutsplus.com/tutorials/understandinghash-functions-and-keeping-passwords-safe--net-17577
Hash-funktioner, enkelt exempel In: lösenord på 6 bokstäver Ut: summan av ingående ACSII-koderna Stort spann på output + kombinatoriskt svårt att reversera Hur fick jag summan 987? Hur många bokstäver ingick? Vilka bokstäver?
Viktigt med stort spann Kan bara reverseras med brute-force attacker Dvs. iterera igenom alla tänkbara lösenord, och se om hash(lösenord) = lagrat lösenord Ju större maxvärde på hash-output, desto fler kombinatoriska möjligheter måste kollas igenom Ett vanligt förstärkningstrick är att köra hash(hash(hash(hash (lösenord))))
11 FEBRUARY 2018 Vanliga funktioner för att kryptera lösenord PBKDF2 Bcrypt Scrypt
11 FEBRUARY 2018 Attacker Har kommit över hashat lösenord Löp igenom alla tänkbara lösenord, testa om det ger samma hash Men, väldigt många tänkbara lösenord Om hashen går att bryta i delar (t.ex. första halvan, andra halvan) meet in the middle attack Löp igenom halva möjligheterna, lagra hash i tabell Löp igenom andra halvan + kombinera från tabell Halv tidsåtgång och halv minnesåtgång
11 FEBRUARY 2018 Regnbågstabeller Även meet in the middle attack tar dock lång tid Tabellen måste genereras Andra halvan måste löpas igenom För att hinna på rimlig tid (timmar/dagar) För-generera kända hash-funktioner på vanliga lösenord T.ex. bcrypt( password ), bcrypt( secure ), bcrypt( secure password ),
Hur kan man stoppa denna typ av attack?
Hantera registrering och lösenord
User User-tabell utökas med några kolumner class User(db.Model): tablename = 'users' id = db.column(db.integer, primary_key=true) name = db.column(db.string(5)) email = db.column(db.string(20), unique=true) passw_hash = db.column(db.string(100))
Flera bra bibliotek för lösenordshashning Flask-bcrypt Itsdangerous (http://pythonhosted.org/itsdangerous/ ) Verkzeug from flask_bcrypt import Bcrypt from werkzeug.security import generate_password_hash, check_password_hash
Registrera användare class User(db.Model): def init (self, name, email, password): self.name = name self.email = email self.pw_hash = # hasha löseordet def register_user(username, email, password): user = User(username, email, password) db.session.add(user) db.session.commit()
Hantera inloggningsstatus
Autentisering Server Registrering Namn, email, lösenord Klient Inloggningsstatus
Autentiseringsflödet klient server logga in anv.namn + lösenord kollar mot lagrat lösenord acess token access token
REST (Representational State Transfer) Viktig aspekt av REST Servern kommer inte ihåg sin interaktion med kilenter från ett request-anrop till ett annat Klienten Håller koll på användarens inloggningstatus Får token från servern Sparar den och skickar med vid varje nytt anrop
Klient: lagrar access token r2 = requests.post(url_root + '/login', json={'email': user_email, 'password': user_pw, 'device': user_device}) access_token = r2.content r3 = requests.post(url_root + '/messages/' + msg_id + '/flag/' + email, headers={'authorization : Bearer + access_token}) Skickas med i header vid anrop där inloggning krävs
Serverns svar på inloggning HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Cache-Control: no-store Pragma: no-cache { "access_token": server-generated.jwt.here", mf_9.b5f-4.1jqm "token_type": Bearer", "expires_in": 3600, "refresh_token": "tgzv3jokf0xg5qx2tlkwia" }
Skicka access token i efterföljande requests GET /resource/1 HTTP/1.1 Host: example.com/messages/ /read_by/ Authorization: Bearer mf_9.b5f-4.1jqm { request-body }
Skapa request med headers url = 'https://api.github.com/some/endpoint' headers = {'user-agent': 'my-app/0.0.1'} response = requests.get(url, headers=headers) Läs mer på: http://docs.pythonrequests.org/en/master/user/quickstart/
Säkerhetsaspekter
Säkerhetsaspekter Tänk om klienten missbrukar token Behåller den alltför länge Säljer den till någon annan Fifflar med den, t.ex. ändrar dess innehåll Tänk om någon kommer över token
Servern signerar access token Skiver under med unik server- secret Secret ska bara servern ha tillgång till Bakar in annan information bäst-före anv.email, etc. Packar ihop allt och krypterar Reversibelt (med hemlig nyckel)
Token är krypterad Mottagaren (klienten) kan inte avkryptera, vet inte secret Vet därför inte vad som har kodats i token Kan inte ändra utan att det märks på server-sidan Servern packar upp och kollar info i medskickad access-token för varje request
Ytterligare info i token Vill ofta ha med andra nyckel-värde par T.ex. { device : user_device, email : user_email} Fördel med JSON: ej förutbestämda nycklar
JSON (JavaScript Object Notation) Text-sträng { } "name":"john", "age":30, "cars": { "car1":"ford", "car2":"bmw", "car3":"fiat" }
JSON Web Tokens (JWT)
JWT (JSON Web Token) Header hashas till xxxxx Payload hashas till yyyyy Signature + payload hashas till zzzzz JWT:n blir xxxxx.yyyyy.zzzzz
12 FEBRUARY 2018 JWT claims i JWT-payload Registrerade (standard) jwt claims jti : jwt_id iss : server_id 'exp': datetime.now() + timedelta(days=0, seconds=50), 'iat': datetime.now(), 'sub': user_email
Hantering av standard claims Om man använder biblioteket flask-jwt-extended: Sätt config-variabler app.config['jwt_access_token_expires'] = datetime.timedelta(minutes=500) app.config['secret_key ] = sdjkfsdkgfgdfgfdh
12 FEBRUARY 2018 Login vyn @app.route('/login', methods=['post']) def login_user(): rdata = request.get_json() email = rdata['email'] password = rdata['password'] try: data.login_user(email, password) token = create_access_token(email) return token
Utloggning
Utloggning innan token exp. har gått ut? T.ex. token exp = en vecka Klienten har token i handen, kan fortsätta skicka med i kommande anrop Måste hitta ett sätt att återkalla token Servern måste hålla reda på vilka token som har återkallats
12 FEBRUARY 2018 Utloggningsvy (utloggningsfunktion) @app.route('/logout', methods=['delete']) @jwt_required def logout(): jti = get_raw_jwt()['jti'] data.revoke_token(jti) return 'Access token revoked', 200
12 FEBRUARY 2018 Blacklisting Hur ska vår server komma ihåg att en token är återkallad (revoke_token)? Blacklisting Tokens som är utloggade sparas i en blacklist Varje inkommande token kollas mot blacklist, förutom den vanliga kollen av exp, etc.
12 FEBRUARY 2018 Blacklist Extra tabell i er databas class Blacklist(db.Model): id = db.column(db.integer, primary_key=true) jti = db.column(db.string(36), nullable=false) def init (self, jti): self.jti = jti
Decorators
Första ansats att kolla inloggning @app.route('/messages/<msg_id>/flag/<email>', methods=['post']) def mark_message_as_read(msg_id, email): if <check_access_token> data.mark_as_read_by(msg_id, email) return 'Message marked as read by ' + email @app.route('/messages', methods=['post']) def save_message(): if < check_access_token > data.store_message(mess_to_store, message_id) return str(message_id)
Nackdelar Upprepning av kod Ändra på många ställen om auth_check ändras Ingen överblick Vilka views vad det nu som kräver inloggning? Lättare att göra misstag Etc.
Snyggare med decorators En wrapper (lindar in en funktion): Gör förbearbetningssteg, dvs. kollar inloggning Vidarebefordrar sedan anropet till funktionen @app.route('/messages', methods=['post']) @jwt_required def save_message(): Tar hand om check_access_token, skickar 401, etc.
Ex. på Flask decorator def xxx_required(f): @wraps(f) def decorated_function(*args, **kwargs): if <check fails>: return <fel> return f(*args, **kwargs) return decorated_function Ersätt med check_access_token, etc. egna anpassningar Läs mer på: http://flask.pocoo.org/docs/0.12/patterns/viewdecorat ors/
jwt-extended biblioteket wrappern jwt_required är färdig-definierad from flask_jwt_extended import jwt_required Men, hur kan man lägga in egen information (t.ex. namn, lösenord) i token, när allt sköts av biblioteket?
Skriv egen plug-in Plug-in anropas när token byggs upp @jwt.user_claims_loader def add_claims_to_access_token(user_email): return { 'email': user_email, 'device': device } Vill ha med dessa i varje token
När du vill hämta info från en token @app.route( ) @jwt_required def my_view( ): claims = get_jwt_claims() email = claims['email ]
OAuth 2.0 Open Authorization protocol
OAuth 2.0: protokoll för inloggning Vilka parter involverade Vilka steg ska genomföras Vad ska skickas i varje steg Bra länkar: https://auth0.com/docs/tokens/idp https://aaronparecki.com/oauth-2-simplified/
Tre parter Resurs-server (hanterar databas, etc.) Klienten (= user agent ) Android-delen av er app (nu fejkad genom requests-anrop) Resurs-ägaren (användaren)
Fyra parter Resurs-server (backend) Klienten (Android-delen) Vill komma åt användarkonto Auth-server (t.ex. Google ID) Visar ett gränssnitt där användaren kan logga in Användare Er app Loggar in och ger tillåtelse för er app att använda (delar av) användarens konto
Autentiseringsflödet auth server klient anv.namn + lösenord server access token kollar mot lagrat lösenord acess token userid access token
Google Sign-In for Android -- och backend server
12 FEBRUARY 2018 Autentiseringsflödet auth server klient public key server access token anv.namn + lösenord kollar mot lagrat lösenord access token public key + access token = OK
Googles auth-server Hanterar två säkerhetsrisker Er app Användaren Båda måste identitetskollas Appen reggas och får en app secret Användaren loggar in, får token
Flöde När användaren (via Android-appen) skickar token Servern skickar vidare till Google:s auth server för kontroll Google:s auth server skickar tillbaks anv.id Servern kan leta upp data och skicka respons till Android-appen
Få konfig.fil till Android-appen Fyll i information om appen i web-formulär Får google-services.json Ladda ner filen och kopiera till egna projektmappen
Få web-server client ID Gå till web-console, fyll i uppgifter Få en Web Server Client ID Kopiera den till ditt projekt Till filen strings.xml: <string name="server_client_id">your_server_client_id</string>
För inloggning inifrån appen Använd fördefinierade metoder Nytt options-objekt Bygg ett googleapiclient(options, )-objekt Be GoogleSignInApi att initialisera ett intent Starta Google Sign-In activitet med detta intent startactivityforresult(singinintent, )
Ta emot och skicka vidare user ID token onactivityresult() lagra <user ID token> // skickas till resersservern vid varje request
Koll av token på server-sidan Använd Googles public key för att verifiera integriteten av access token Googles python bibliotek: https://developers.google.com/api-clientlibrary/python/start/installation Läs mer på: https://developers.google.com/apiclient-library/python/start/installation
Läs mer om Google Sign-In for Android https://developers.google.com/identity/signin/android/start
rita.kovordanyi@liu.se www.liu.se