Flask Passwort vergessen Funktion

Django, Flask, Bottle, WSGI, CGI…
Antworten
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Hallo zusammen. Ich mache mich deswegen schon eine Weile verrückt, weil es einerseits simpel ist, andererseits ist ein Fehler hier unbeschreiblich Schlimm.

Ich habe jetzt meine Lösung fertig:

1. Es gibt eine Passwort vergessen Seite
- Der Nutzer gibt seine Email, auf die die reset URL gesendet werden soll
- Es wird geprüft ob der Nutzer mit dieser Email existiert
- Es wird geprüft ob die Email verifiziert wurde (damals als er sich angemeldet hat)
- Es wird in der DB ein neuer reset password request angelegt
- Email wird versendet

Code:

Code: Alles auswählen

if form.validate_on_submit():
        
        user = User.query.filter_by(email=form.email.data.lower().strip()).first()
        
        if user is None:
            flash('Nutzer existiert nicht', 'error')
            return redirect(url_for('forgot_pw'))
        
        if user.email_verified is False:
            flash('Diese Email ist nicht verifiziert', 'error')
            return redirect(url_for('forgot_pw'))
        
        new_reset_req = ResetPasswordRequest(token=get_random_code(30), date_added=datetime.today(), requested_by=form.email.data.lower().strip(),
                                             user_id=user.id)
        
        db_session.add(new_reset_req)
        db_session.commit()
        
        reset_token = generate_confirmation_token(new_reset_req.token)
        reset_url = url_for('get_new_pw', token=reset_token, _external=True)
        html = render_template('emails/forgot_pw_email.html', reset_url=reset_url)
        subject = "Passwort vergessen bei Monteurzimmer-1A"
        send_email(user.email, subject, html, None)
        
        flash('Wir haben Ihnen eine Email gesendet mit einem Änderungslink', 'success')
        return redirect(url_for('forgot_pw'))
So erstelle ich das Token:

Code: Alles auswählen

def generate_confirmation_token(email):
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
2. In der Email muss der User auf den Link klicken, dieser führt den User zur reset password Seite
- Hier wird das Token gecheckt, falls damit etwas nicht stimmt, dann lädt die Seite nicht
- Falls die Token Lebensdauer von 15 min. überschritten ist, dann lädt die Seite nicht
- Jetzt wird der reset request von der DB geholt über das token
- Es wird geprüft ob der reset request bereits eingelöst wurde, wenn ja gibt es eine Fehlermeldung
- Ansonsten kann jetzt das neue Passwort vergeben und gespeichert werden

Code:

Code: Alles auswählen

# Forgot password set new
@app.route('/passwort-vergessen/<token>', methods=["GET","POST"])
def get_new_pw(token):
    
    form = GetNewPWForm()
    form_contact_us = ContactForm(prefix="contact-us-form")
    
    try:
        token_status = confirm_token_for_pw(token)
        
        if token_status is False:
            count_error('ForgotPWTokenStatusFalse')
            flash('Dieser Verifizierungslink ist nicht mehr gültig. Lassen Sie sich einen neuen Verifizierungslink zusenden', 'error')
            return redirect(url_for('forgot_pw'))
        else:
            print (token_status)
            if form.validate_on_submit():
                
                check_link = ResetPasswordRequest.query.filter_by(token=token_status).first()
                
                if check_link is None:
                    flash('Dieser Verifizierungslink ist nicht mehr gültig. Lassen Sie sich einen neuen Verifizierungslink zusenden', 'error')
                    return redirect(url_for('forgot_pw'))
                
                if check_link.date_used is not None:
                    flash('Dieser Verifizierungslink wurde bereits eingelöst', 'error')
                    return redirect(url_for('forgot_pw'))
                
                setattr(check_link, "date_used", datetime.today())
                
                user = User.query.filter_by(id=check_link.user_id).first()
                
                if user is None:
                    flash('Der Nutzer existiert nicht', 'error')
                    return redirect(url_for('forgot_pw'))
                
                setattr(user, "password", make_hash(form.password.data)) 
                db_session.commit()
                flash('Passwort wurde geändert', 'success')
                return redirect(url_for('login_a_user'))
            return render_template('reset_password.html', form_contact_us=form_contact_us, form=form)
        
    except Exception as e:
        print ('token verify error', e)
        count_error('ForgotPWTokenError')
        flash('Dieser Verifizierungslink ist nicht mehr gültig. Lassen Sie sich einen neuen Verifizierungslink zusenden', 'error')
        return redirect(url_for('forgot_pw'))
confirm_token_for_pw Sieht so aus:

Code: Alles auswählen

def confirm_token_for_pw(token, expiration=1800): # old expiration was 3600 seconds = 1 hour
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    try:
        confirm_token = serializer.loads(
            token,
            salt=app.config['SECURITY_PASSWORD_SALT'],
            max_age=expiration
        )
    except:
        return False
    #print (confirm_token)
    return confirm_token
Ach und hier ist das reset password element in der Datenbank:

Code: Alles auswählen

class ResetPasswordRequest(Base):
    __tablename__ = 'resetpasswordrequests'
    id = Column(Integer, primary_key=True)
    token = Column(Text, nullable=False, unique=True)
    date_added = Column(DateTime(timezone=True), nullable=False)
    date_used = Column(DateTime(timezone=True), nullable=True)
    requested_by = Column(Text, nullable=False)
    # Foreign key
    user_id = Column(Integer, ForeignKey('users.id'))
    ResetPasswordRequest_addresses_User = relationship("User", back_populates="User_addresses_ResetPasswordRequest")
Ich habe jetzt viel gelesen, dass Email eigentlich immer die Schwachpunkte sind und hier gebe ich die Möglichkeit jedem, der den Link kennt das Passwort zu ändern.
Das muss wohl so sein, aber es ist irgendwie kein gutes Gefühl. Ich verstehe, dass man den reset link nicht erraten kann, dieser ist nirgendswo verlinkt, aber jemand könnte ihn aus der Email klauen?

Ich nutze bei allen Mails:

Code: Alles auswählen

MAIL_USE_SSL = True
Ich will nur wissen, ob meine Lösung in Ordnung ist oder habe ich etwas Offensichtliches vergessen?

Ich habe auch gelesen, dass man einen token hash speicher kann, aber ich verstehe nicht wozu.
Der Link in der Email SOLL ja für 'anonyme' User funktionieren und das Passwort ändern können.


Wenn sich jemand die Zeit nimmt, um sich das anzugucken, wäre ich sehr dankbar.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Ich würde HMAC in der URL verwenden und als Wert z.B. die IP-Adresse (vom Rechner der den "PW-Zurücksetzen-Mechanismus" in Gang gesetzt hat) oder den Benutzernamen integrieren. Damit kannst du relativ sicher sein, dass der URL-"Klicker" auch der ist, der die Reset-URL angefordert hat. Alternativ kann man auch noch ein zusätzlich eine persönliche Frage beantworten lassen, wenn die Paranoia größer ist.

Eine weitere alternative ist das setzen eines Device Cookies. Oder du implementierst Zweifaktor.
When we say computer, we mean the electronic computer.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

sls hat geschrieben: Montag 28. Januar 2019, 20:10 Ich würde HMAC in der URL verwenden und als Wert z.B. die IP-Adresse (vom Rechner der den "PW-Zurücksetzen-Mechanismus" in Gang gesetzt hat) oder den Benutzernamen integrieren. Damit kannst du relativ sicher sein, dass der URL-"Klicker" auch der ist, der die Reset-URL angefordert hat. Alternativ kann man auch noch ein zusätzlich eine persönliche Frage beantworten lassen, wenn die Paranoia größer ist.

Eine weitere alternative ist das setzen eines Device Cookies. Oder du implementierst Zweifaktor.
Aha, d.h. ich hashe die device ip in die URL beim Erstellen des reset links und wenn es an reseten geht, nehme ich mir die IP aus der URL und gucke nochmal ob es wirklich das Gerät ist, was den Reset durchführt? Gute Idee. Werde ich machen.

Das mit dem Benutzername integrieren verstehe ich nicht. Wenn ich den Nutzername 'Anton' mit hashe, was bringt mir das beim Reseten? Der resetende User ist eigentlich anonym. Ich kann 'Anton' mit nichts abgleichen.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich denke es geht eher um eine eingabe auf der Reset Seite. Wenn du fürchtest eine man in the Middle Attacke hat die E-Mail kompromittiert, könntest du theoretisch eine Frage stellen, die den User identifiziert. Das ist sozusagen 1.5 Faktor. Ich würde mir allerdings da nicht so einen Kopf machen. Mit der Mail alleine fährst du schon gut. Und wirklich besser wird’s wie vorgeschlagen nur mit 2 Faktor, und das ist ein anderer Schnack, weil üblicherweise Geld gezahlt werden muss. Für sms.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

__deets__ hat geschrieben: Dienstag 29. Januar 2019, 00:03 Ich denke es geht eher um eine eingabe auf der Reset Seite. Wenn du fürchtest eine man in the Middle Attacke hat die E-Mail kompromittiert, könntest du theoretisch eine Frage stellen, die den User identifiziert. Das ist sozusagen 1.5 Faktor. Ich würde mir allerdings da nicht so einen Kopf machen. Mit der Mail alleine fährst du schon gut. Und wirklich besser wird’s wie vorgeschlagen nur mit 2 Faktor, und das ist ein anderer Schnack, weil üblicherweise Geld gezahlt werden muss. Für sms.
Vielen Dank.
Ich mache das mit der session oder der IP. Sobald man sich den Link zusenden lässt, wird eine random session variable generiert und mit in der DB gespeichert. Beim Reseten wird dann verglichen ob die session stimmt. Ich denke das zu umgehen dürfte sehr schwierig sein. Hierfür bräuchte man die session des Absendenden und die Email.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hm. Ich bin da skeptisch mit diesen Maßnahmen. Wenn ich mich zb auf meinem Linux Rechner einloggen will, das Passwort nicht mehr kenne, dann einen Link anfordere - dann lese und klicke ich den auf meinem iPhone. Weil ich kein Mail eingerichtet habe auf dem PC. Da fallen dann sowohl IP als auch Session flach. Da kann man natürlich der Meinung sein das wäre ein Randfall. Realität ist es trotzdem. Wir haben nicht mehr nur einn Computer unterm Treppenabsatz stehen.

Wenn du sowas also machst, dann erklär den Leuten, warum es fehlschlägt, und sag ihnen sie sollen den Link am gleichen Gerät anfordern.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

__deets__ hat geschrieben: Dienstag 29. Januar 2019, 08:53 Hm. Ich bin da skeptisch mit diesen Maßnahmen. Wenn ich mich zb auf meinem Linux Rechner einloggen will, das Passwort nicht mehr kenne, dann einen Link anfordere - dann lese und klicke ich den auf meinem iPhone. Weil ich kein Mail eingerichtet habe auf dem PC. Da fallen dann sowohl IP als auch Session flach. Da kann man natürlich der Meinung sein das wäre ein Randfall. Realität ist es trotzdem. Wir haben nicht mehr nur einn Computer unterm Treppenabsatz stehen.

Wenn du sowas also machst, dann erklär den Leuten, warum es fehlschlägt, und sag ihnen sie sollen den Link am gleichen Gerät anfordern.
Ja das würde ich natürlich erklären, in so einem Fall würde der User sich bei uns melden + ich würde ich in der DB markieren. Ich denke aber, erstens passieren passwort resets relativ selten, zweitens noch seltener ist es, dass dein Fall eintritt und man ein anderes Gerät nutzt. Dennoch danke für die Warnung!
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

__deets__ hat geschrieben: Dienstag 29. Januar 2019, 08:53 Hm. Ich bin da skeptisch mit diesen Maßnahmen. Wenn ich mich zb auf meinem Linux Rechner einloggen will, das Passwort nicht mehr kenne, dann einen Link anfordere - dann lese und klicke ich den auf meinem iPhone. Weil ich kein Mail eingerichtet habe auf dem PC. Da fallen dann sowohl IP als auch Session flach. Da kann man natürlich der Meinung sein das wäre ein Randfall. Realität ist es trotzdem. Wir haben nicht mehr nur einn Computer unterm Treppenabsatz stehen.
Du bist also mit deinem Iphone nicht im gleichen lokalen Netz wie dein Linux-Rechner? ;-)

Aber selbst wenn: du loggst dich mit deiner Linux-Kiste ein, forderst von der Webseite einen Passwort-Reset-Link an, in der Zwischenzeit explodiert dein Linux-Rechner, du nimmst dein Iphone welches außerdem nicht im selben lokalen Netz ist (weshalb sich dann auch die öffentliche IP-Adresse unterscheidet) oder du machst das ganze früh morgens und fällst genau in den Timeslot in der dein ISP deine public IP resettet... Also wenn bei mir das Zurücksetzen eines Passwortes nicht funktioniert, fordere ich eben einen zweiten Link an. Das hat bisher dann immer funktioniert.

Ich würde aber auch keinen zu großen Aufriss davon machen. SMS soll ja nicht mehr so toll sein, ich versuche für meine Authentifizierungen Zweifaktor mit Hardware-Tokens zu implementieren, der Aufwand ist relativ aber IMHO gesünder. Wir können zum Mond fliegen, aber Online-Banking nutzt überwiegend immer noch keinen zweiten Faktor :/
When we say computer, we mean the electronic computer.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@sls: Also mein Smartphone ist relativ häufig nicht im gleichen lokalen Netz wie der PC vor dem ich sitze. Ist das wirklich ungewöhnlich? Mein monatliches Datenvolumen reicht locker für das bisschen Mail und Chat und zusätzlich Wifi anschalten verbraucht nur Akku, bringt aber ansonsten nicht wirklich Vorteile.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

sls hat geschrieben: Dienstag 29. Januar 2019, 10:23 Du bist also mit deinem Iphone nicht im gleichen lokalen Netz wie dein Linux-Rechner? ;-)

Aber selbst wenn: du loggst dich mit deiner Linux-Kiste ein, forderst von der Webseite einen Passwort-Reset-Link an, in der Zwischenzeit explodiert dein Linux-Rechner, du nimmst dein Iphone welches außerdem nicht im selben lokalen Netz ist (weshalb sich dann auch die öffentliche IP-Adresse unterscheidet) oder du machst das ganze früh morgens und fällst genau in den Timeslot in der dein ISP deine public IP resettet... Also wenn bei mir das Zurücksetzen eines Passwortes nicht funktioniert, fordere ich eben einen zweiten Link an. Das hat bisher dann immer funktioniert.
Kommt durchaus vor. Weil ich mit dem iPhone schlechte Verbindung zwei Stunden vorher im Cafe hatte, und das WIFI fuer den Tag aus ist. Oder iOS genau diese Entscheidung auch mal selbst gefaellt hat. Oder weil ich auf Arbeit bin.

Und ich habe ja auch nicht gesagt, es NICHT zu machen. Nur sollte man klar kommunizieren, an welcher Stelle es nun hakt. Ein "geht nicht, is nicht" das einem nicht verraet, woran es liegt, hilft halt keinem.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Gut, das artet etwas aus. Ich nutze nur dann das Mobilfunknetz wenn ich tatsächlich unterwegs bin oder mich auf einer Webseite authentifzieren will von unterwegs, ohne dabei das free wifi zu verwenden was einem überall so angeboten wird. Wenn ich das Zugangspasswort zu einer Seite vergessen habe merke ich das meistens genau dann wenn ich mich authentifzieren möchte, das Passwort-Reset führe ich dann adhoc durch und da (wie im Beispiel oben) so ein Reset-Link ja auch verfallen kann / sollte, bestätige ich diesen meisten binnen von Minuten (zumal ich bei sowas nie ein gutes Gefühl habe so lange ich nicht ein neues Passwort gesetzt habe)
When we say computer, we mean the electronic computer.
Antworten