Skip to content

Conversation

@lilioid
Copy link

@lilioid lilioid commented Oct 22, 2023

closes #16

Wie erwähnt implementiere ich in dieser PR login mit OpenidConnect.
Das Login-Formular sieht nun dadurch so aus:
image

Zusätzlich habe ich noch implementiert, dass die flash-messages jetzt die kateogrien info, warning und error in unterschiedlichen farben anzeigen, weil ich die nutze, wenn der openid login aus Gründen nicht funktioniert.

Implementationsdetails:

  • Ich verwende hierfür die Library simple_openid_connect, die bei uns an der Uni-Hamburg von der Fachschaft entwickelt wird und so grundsätzliche Dinge übernimmt, wie die Validierung von Tokens, das parsen der verschiedenen URL-Parameter und dem Code-for-Token-Exchange.
  • Die Implementation verwendet den authorization code flow (Keycloak nennt den Standard Flow).
  • Die redirect-url, die für OIDC-Login benötigt wird, wird automatisch aus der aktuellen request generiert, wenn /login das erste mal aufgerufen wird (app.py:90)
  • Es gibt ein paar neue einstellungen, die aus config.json eingelesen werden.
    • OIDC_ISSUER z.B. https://auth.example.com/realms/queerlexikon für Keycloak. Hieraus wird automatisch das Openid Autokonfigurations-Dokument abgerufen, um die verschiedenen openid endpoints zu entdecken.
    • OIDC_CLIENT_ID
    • OIDC_CLIENT_SECRET
    • OIDC_SCOPE defaulted auf openid und ist der scope, der vom openid issuer requested wird.
  • Der Hauptteil des logins passiert im login_oidc_callback (app.py:67). Da ich mir nicht ganz sicher war, wie das users = {} dictionary befüllt wird, aus dem load_user() das user objekt läd, kann es sein, dass der Teil entsprechend nicht richtig funktioniert (app.py:78). Insbesondere, wenn die Keycloak IDs nicht die gleichen sind, wie die, die hier als id bezeichnet werden kann das noch kaputt sein.

@aurorasmiles
Copy link
Member

Beim lokalen Testen klappt der Login bei mir nicht, stattdessen bekomme ich

ERROR:app:Exception on /login-callback [GET]
Traceback (most recent call last):
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\flask\app.py", line 2190, in wsgi_app
    response = self.full_dispatch_request()
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\flask\app.py", line 1486, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\flask\app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\flask\app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\app.py", line 71, in login_oidc_callback
    auth_result = oidc.authorization_code_flow.handle_authentication_result(
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\simple_openid_connect\flows\authorization_code_flow\client.py", line 86, in handle_authentication_result
    return impl.handle_authentication_result(
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\simple_openid_connect\flows\authorization_code_flow\__init__.py", line 85, in handle_authentication_result
    return exchange_code_for_tokens(
  File "C:\Users\Aurora\Documents\projects\QueerLexikon\passworttool\venv\lib\site-packages\simple_openid_connect\flows\authorization_code_flow\__init__.py", line 130, in exchange_code_for_tokens
    return TokenSuccessResponse.parse_raw(response.content)
  File "pydantic\main.py", line 549, in pydantic.main.BaseModel.parse_raw
  File "pydantic\main.py", line 526, in pydantic.main.BaseModel.parse_obj
  File "pydantic\main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for TokenSuccessResponse
id_token
  field required (type=value_error.missing)

habe ich da was vergessen?

@lilioid
Copy link
Author

lilioid commented Oct 24, 2023

habe ich da was vergessen?

Ne eigentlich nicht.
Aber es sieht so aus als ob dein OpenID provider in der Token Response kein id token mitsendet. Das ist aber in OpenID eigentlich verpflichtend (hier der spec dazu).

Ich habe das eigentlich auch mit meinem eigenen Keycloak getestet, wo der Teil funktioniert hat. Hast du die Response irgendwie gemocked oder was nutzt du als OpenID provider zum testen?

@aurorasmiles
Copy link
Member

Ich hab das mit unserem keycloak getestet😅

@lilioid
Copy link
Author

lilioid commented Oct 25, 2023

Das ist.. ähmm.. interessant 😅

Ich habe mal ein bisschen rumgegoogelt diesen StackOverflow gefunden.
Das klingt so als ob Kecloak kein ID-Token ausstellt, wenn der openid scope nicht requested wird oder wenn all client scopes unassigned sind.
Magst du mal verifizieren, dass du in der app config nicht ausversehen, den openid scope nicht requestest oder in der keycloak client config irgendwie all client scopes unassigned hast?


Ansonsten hier einmal eine exportierte keycloak client config, die bei mir funktioniert (musst du nicht importieren aber, wenn du in Keycloak in den Client Einstellungen oben rechts auf Action -> Export drückst kannst du ja mal vergleichen).

Und hier nochmal der Teil der app config.json, der dazu gehört:

  "OIDC_ISSUER": "https://auth.ftsell.de/realms/master",
  "OIDC_CLIENT_ID": "passworttool-test",
  "OIDC_CLIENT_SECRET": "...",
  "OIDC_SCOPE": "openid"

@aurorasmiles
Copy link
Member

Ah, so komme ich weiter. Wenn ich jetzt als Scope openid requeste bekomme ich zumindest keinen Serverfehler mehr zurück, aber ein Login with Keycloak did not succeed: Could not load user object for user id that was received from Keycloak.

@lilioid
Copy link
Author

lilioid commented Oct 25, 2023

Supi das ist soweit erwartet.

Ich wusste wie gesagt nicht, wie dieses users = {} objekt befüllt wird. meines erwartens nach sollte load_user() doch bestehende user aus dem ldap laden oder nicht?

@aurorasmiles
Copy link
Member

Eventuell fehlt hier noch eine config-Variable für die Zuweisung welches Attribut aus Keycloak richtig ist?

@xenein
Copy link
Member

xenein commented Oct 25, 2023

Das Flask-Login ist sehr agnostisch über sein Environment. Es braucht irgendeine Sammlung von User-Objekten, die von UserMixin erben und benutzt Decorators um diese Sammlung zu verwalten.

Bisher so:
Anlegen:

passworttool/app.py

Lines 48 to 52 in 448bbb0

@ldap_manager.save_user
def save_user(dn, username, data, memberships):
user = User(dn, username, data)
users[dn] = user
return user

Laden:

passworttool/app.py

Lines 41 to 46 in 448bbb0

@login_manager.user_loader
def load_user(id):
if id in users:
return users[id]
else:
return None

Da verwenden wir bisher direkt die Daten, wie sie flask_ldap3_login ausgibt.

Im Prinzip kann das User-Objekt, da das Passworttool und seine Daten sonst nirgends von Relevanz sind, das User-Objekt neu gestalten und auch die dekorierten Funktionen für den user_loader und save_user festlegen.

Für die App relevant ist ein Name und die hinterlegte Mailadresse. Das sollte im Prinzip aus dem Auth rausfallen und dann sowas ermöglichen

class User(UserMixin):
    def __init__(self, oidc_id_token):
        self.id = oidc_id_token.sub
        self.username =

Oder aber, da wir hier ohnehin lokal nichts persistentes brauchen, ohne Flask-Login arbeiten und schlicht die relevanten Daten (Name und Mailadresse) aus dem id_token in die flask_session - und statt @login_required anhand der Inhalt der Session unterscheiden. Da Flask-Session mit signed cookies arbeitet, sollte das safe genug sein.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hacktoberfest: Login über OIDC

3 participants