Exkurs: OAuth2 und OpenID Connect
Einige Websites bieten die Option sich z.B. mit einem bestehenden Google-, Facebook- oder GitHub-Account anzumelden.
Die herkömmliche Methode sich mit Benutzernamen
und ein Kennwort anzumelden, hat einige Nachteile:
- Passwörter müssen sicher gespeichert werden (Satled Hashes)
- Kontobezogene Funktionen müssen implementiert werden:
Anmeldung, Kennwort zurücksetzen, Zwei-Faktoren-Authentifizierung usw. - Böswillige Anmeldungen müssen abgewehrt werden
- Nutzer müssen sich einen weiteren Benutzernamen und ein weiteres Kennwort merken.
Wenn Nutzerkonten anderer Anbieter zugelassen werden, wird die Verantwortung auf diese Anbieter übertragen. Die Anwendung wartet darauf, dass der Nutzer die Authentifizierung beim Anbieter durchläuft. Dieser informiert dann die Anwendung über diesen Nutzer. Dann kann der Nutzer angemeldet werden.
Es brauchen keine Kennwörter gespeichert werden und der Anbieter kümmert sich um die Sicherheit.
Dabei werden zwei wichtige und weit verbreitete Spezifikationen angewendet:
OAuth2 und OpenID Connect (OIDC)
OIDC erweitert dabei OAuth2 um einige neuere Konzepte. Beide Spezifikationen definieren wie eine Anwendung als Client (3rd party application) von einem Authentifiikationsprovider Informationen erhält. Das erfordert normalerweise das Einverständnis des betroffenen Benutzers.
Die Webanwendung ist hier der Fremdanwendung (auch 3rd party application) und hat im Folgenden einen Button , um sich z.B. über Google anzumelden. Der Authentifikationsprovider muss das natürlich wissen und bietet daher die Möglichkeit an, die Anwendung als Client bei Google zu registrieren.
Der Button leitet den Nutzer zu z.B. zu Google weiter. Google stellt sicher, dass der Nutzer einverstanden ist, dass seine Daten wie z.B. E-Mail-Adresse o.a. zur Anwendung übertragen werden dürfen. Diese Daten werden zur Anwendung übertragen und dort gespeichert. Damit kann er bei der Anwendung angemeldet werden.
Diese Schritte werden als handshake, flow oder auch dance bezeichnet:
OAuth2
-
Eine Drittanwendung wird als Client beim Anbieter (Provider) registriert.
- Die Anwendung erhält vom Anbieter eindeutige Client-Anmeldeinformationen.
- Diese Client-Anmeldeinformationen werden benötigt, um den Client (Anwendung) später beim Anbieter zu authentifizieren.
- Der Client sendet eine Anfrage an die Autorisierungs-URL des Anbieters.
- Der Anbieter fordert den Benutzer auf, sich zu authentifizieren (nachzuweisen, wer er ist).
- Der Anbieter bittet den Benutzer um Zustimmung, dass der Client in seinem Auftrag Informationen verlangt.
- Der Anbieter sendet dem Client einen eindeutigen Autorisierungscode.
- Der Client sendet den Autorisierungscode zurück an die Token-URL des Anbieters.
- Der Anbieter versendet die Benutzerinformationen zur Verwendung mit anderen Anbieter-URLs im Auftrag des Benutzers.
OpenID Connect
OIDC baut auf OAuth2 auf und fügt einige zusätzliche Funktionen und Anforderungen hinzu, die hauptsächlich den Authentifizierungsprozess betreffen. Wichtigen OIDC-Konzepte sind die Anbieterkonfiguration (provider configuration) und der Benutzerinfo-Endpunkt (userinfo endpoint).
Die Anbieterkonfiguration enthält Informationen zum Anbieter, einschließlich der genauen URLs, die für den OAuth2-Flow verwendet werden müssen. Es gibt eine Standard-URL bei einem OIDC-Anbieter, mit der ein Dokument mit standardisierten Feldern zurückerhalten werden kann.
Der Benutzerinfo-Endpunkt gibt Informationen über den Benutzer zurück, nachdem der OAuth2-Flow durchlaufen wurde. Dazu gehören E-Mail-Adresse und einige grundlegende Profilinformationen, die in der Anwendung verwendet werden sollen. Um diese Benutzerinformationen zu erhalten, wird das Token vom Anbieter benötigt, wie im letzten Schritt im obigen Ablauf beschrieben.
Im folgenden UML-Anwendungsfalldiagramm wird der gesamte Ablauf visualisiert:

OAuth2 und OpenID Connect
Beispielanwendung
Die folgenden Code-Auszüge bilden den OAuth2-Flow bei dem Authentication Provider Google ab. Mit der Registrierung der Webapplikation bei Google (Google-Konto erforderlich) erhält man eine Client-ID und einen Client-Key mit dem sich die Webapplikation später beim Authentication Provider selbst authentifizieren muss. Daher wird beides hier in die Datei settings.py kopiert:
...
# Google Auth-Client GOOGLE_CLIENT_ID = "YourGoogleAuthClientID" GOOGLE_CLIENT_KEY = "YourGoogleAuthClientKey" GOOGLE_DISCOVERY_URL = ("https://accounts.google.com/.well-known/openid-configuration")
...
Auszug aus settings.py
Das Klicken auf den Google-Anmelde-Button wird auf die @app.route("/googleLogin") verlinkt, welche auf die Request URI von Google weiterleitet.
Der erste Parameter ist dabei die URL des Authorisation-Endpunkts, die interessanterweise nicht direkt in settings.py festgelegt wird, sondern dort wird die Google Discovery URL https://accounts.google.com/.well-known/openid-configuration festgelegt, die die Endpunkte von Google als JSON-Response liefert.
Weitere Parameter sind die URL der Callback-Route, über die der Authentification-Provider die Webapplikation informiert, das die Anmeldedaten des Nutzer bereitstehen und der Scope, also diejenigen Daten die vom Provider geliefert werden sollen. Letzeres ist hier lediglich die E-Mail-Adresse. Dieser Scope muss bei der Registrierung der Webapplikation bei Google bereits festgelegt werden.
Die Route @app.route("/googleLogin/callback") erhält von Google den Authorisierungscode, der die Webapplikation zusammen mit Client-Key und Client-ID bei Google authentifiziert und authorisiert wird, die Nutzerdaten zu erhalten.
Abschließend wird im Programmbeispiel die E-Mail-Adresse mit der Rolle Viewer in der Datenbank gespeichert und Anmeldung in der Webapplikation durch Ausstellung eines JWT im Cookie erfolgreich abgeschlossen:
... import requests from oauthlib.oauth2 import WebApplicationClient ... import settings ... # OAuth 2 client setup client = WebApplicationClient(settings.GOOGLE_CLIENT_ID) def get_google_provider_cfg(): # get google openId endpoints return requests.get(settings.GOOGLE_DISCOVERY_URL).json() @app.route("/googleLogin") def login(): # Find out what URL to hit for Google login google_provider_cfg = get_google_provider_cfg() authorization_endpoint = google_provider_cfg["authorization_endpoint"] # Use library to construct the request for Google login and provide # scopes that let you retrieve user's profile from Google request_uri = client.prepare_request_uri( authorization_endpoint, redirect_uri=request.base_url + "/callback", scope=["email"], ) return redirect(request_uri) @app.route("/googleLogin/callback") def callback(): # Get authorization code Google sent back to you code = request.args.get("code") # Find out what URL to hit to get tokens that allow you to ask for # things on behalf of a user google_provider_cfg = get_google_provider_cfg() token_endpoint = google_provider_cfg["token_endpoint"] # Prepare and send a request to get tokens! token_url, headers, body = client.prepare_token_request( token_endpoint, authorization_response=request.url, redirect_url=request.base_url, code=code ) token_response = requests.post( token_url, headers=headers, data=body, auth=(settings.GOOGLE_CLIENT_ID, settings.GOOGLE_CLIENT_KEY), ) # Parse the tokens! client.parse_request_body_response(json.dumps(token_response.json())) # Now that you have tokens (yay) let's find and hit the URL # from Google that gives you the user's profile information, # including their Google profile image and email userinfo_endpoint = google_provider_cfg["userinfo_endpoint"] uri, headers, body = client.add_token(userinfo_endpoint) userinfo_response = requests.get(uri, headers=headers, data=body) # You want to make sure their email is verified. # The user authenticated with Google, authorized your # app, and now you've verified their email through Google! if userinfo_response.json().get("email_verified"): users_email = userinfo_response.json()["email"] else: return "User email not available or not verified by Google.", 400 # Create a user in db with the information provided by Google if not already exists db1 = db.Db() query = "SELECT userID FROM tblUser WHERE username = '%s'" %(users_email) result = db1.execute(query) if len(result) == 0: query = "INSERT INTO tblUser SET username = '%s'" %(users_email) result = db1.execute(query) db1.commit() # only role "Viewer" will be given query = "SELECT userID FROM tblUser WHERE username = '%s'" %(users_email) result = db1.execute(query) if(result): userId = result[0][0] # i.e. result = [(2,)] query = "INSERT INTO tblRoleUser VALUES (%d, 2)" %(userId) result = db1.execute(query) db1.commit() del db1 # close db connection # Login user by creating jwt expiry = dt.datetime.now(tz=timezone.utc) + dt.timedelta(seconds=10) # Expiration 10 seconds in the future token = jwt.encode({"exp": expiry, "roleIDs": [2] }, private_key, algorithm="RS256") # Send user back to homepage resp = make_response(render_template('dashboard.html')) resp.set_cookie('token', token, httponly=True, secure=True) return resp ... if __name__ == '__main__': app.run(ssl_context="adhoc") # run using https to ensure an encrypted connection with Google (pip install pyOpenSSL)
Die Abmeldung erfolgt durch das Löschen des Cookies im Client wie bisher. Es ist nicht nötig und nicht vorgesehen, hierfür den Authentication Provider anzurufen.
Quelle:
https://realpython.com/flask-google-login/ (01.02.2022)