Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Okta login is broken #2291

Open
Lewiscowles1986 opened this issue Dec 7, 2024 · 4 comments · May be fixed by #2292
Open

Okta login is broken #2291

Lewiscowles1986 opened this issue Dec 7, 2024 · 4 comments · May be fixed by #2292

Comments

@Lewiscowles1986
Copy link

Lewiscowles1986 commented Dec 7, 2024

If you'd like to report a bug in Flask-Appbuilder, fill out the template below. Provide
any extra information that may be useful

Responsible disclosure:
We want to keep Flask-AppBuilder safe for everyone. If you've discovered a security vulnerability
please report to [email protected].

Environment

Flask-Appbuilder version: Flask-AppBuilder==4.5.2

pip freeze output:

apispec==6.8.0
attrs==24.2.0
Authlib==1.3.2
babel==2.16.0
blinker==1.9.0
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.4.0
click==8.1.7
colorama==0.4.6
cryptography==44.0.0
Deprecated==1.2.15
dnspython==2.7.0
email_validator==2.2.0
Flask==2.3.3
Flask-AppBuilder==4.5.2
Flask-Babel==2.0.0
Flask-JWT-Extended==4.7.1
Flask-Limiter==3.9.2
Flask-Login==0.6.3
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.2.2
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
limits==3.14.1
markdown-it-py==3.0.0
MarkupSafe==3.0.2
marshmallow==3.23.1
marshmallow-sqlalchemy==0.28.2
mdurl==0.1.2
ordered-set==4.1.0
packaging==24.2
prison==0.2.1
pycparser==2.22
Pygments==2.18.0
PyJWT==2.10.1
python-dateutil==2.9.0.post0
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rich==13.9.4
rpds-py==0.22.3
six==1.17.0
SQLAlchemy==1.4.54
SQLAlchemy-Utils==0.41.2
typing_extensions==4.12.2
urllib3==2.2.3
Werkzeug==3.1.3
wrapt==1.17.0
WTForms==3.2.1

Describe the expected results

Tell us what should happen.

For a start given the metadata URL other Urls should be discovered, and weirdly it looks like they are in some places, and not others when stepping through in a debugger... Anyway I've got this to the point now where it fails when reading userinfo, but I CURL'ed the endpoint with POST (not GET) and boom, Instant user information...

Minimal reproducible

from flask import Flask, redirect, url_for, session, jsonify
from flask_appbuilder import AppBuilder, SQLA
from authlib.integrations.flask_client import OAuth
from flask_appbuilder.security.manager import AUTH_OAUTH

# Initialize Flask app
app = Flask(__name__)
app.config["SECRET_KEY"] = "your_secret_key_here"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# Okta OAuth configuration
OKTA_CLIENT_ID = "<okta-client-id>" # Replace with your Okta APP Client ID
OKTA_CLIENT_SECRET = "<okta-client-secret>" # Replace with your Okta APP Client Secret
OKTA_ISSUER_URL = "https://<your-sub-domain>.okta.com"  # Replace with your Okta Issuer URL
OKTA_OAUTH_BASE_URL = f"{OKTA_ISSUER_URL}/oauth2/v1"

# AppBuilder Security Config
app.config["AUTH_TYPE"] = AUTH_OAUTH
app.config["AUTH_USER_REGISTRATION"] = True  # Allow automatic user registration
app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"  # Default role
app.config["SQLALCHEMY_ECHO"] = True
app.config["OAUTH_PROVIDERS"] = [
    {
        "name": "okta",
        "icon": "fa-circle-o",  # Icon for the login button
        "token_key": "access_token",  # Field in OAuth token for access
        "remote_app": {
            "client_id": OKTA_CLIENT_ID,
            "client_secret": OKTA_CLIENT_SECRET,
            "api_base_url": OKTA_OAUTH_BASE_URL,
            "server_metadata_url": f"{OKTA_ISSUER_URL}/.well-known/openid-configuration",
            "client_kwargs": {"scope": "openid profile email groups", "token_endpoint_auth_method": "client_secret_post"},
            "access_token_url": f"{OKTA_OAUTH_BASE_URL}/token",
            "authorize_url": f"{OKTA_OAUTH_BASE_URL}/authorize",
        },
    }
]
app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = True

# Initialize database and OAuth
db = SQLA(app)
appbuilder = AppBuilder(app, db.session)
oauth = OAuth(app)

# Configure the Okta OAuth integration
okta = oauth.register(
    name="okta",
    client_id=OKTA_CLIENT_ID,
    client_secret=OKTA_CLIENT_SECRET,
    api_base_url=OKTA_OAUTH_BASE_URL,
    server_metadata_url=f"{OKTA_ISSUER_URL}/.well-known/openid-configuration",
    client_kwargs={"scope": "openid profile email groups"},
    access_token_url=f"{OKTA_OAUTH_BASE_URL}/token",
    authorize_url=f"{OKTA_OAUTH_BASE_URL}/authorize",
)

# Home page route
@app.route("/")
def home():
    return redirect(url_for("login"))

# Login route
@app.route("/login")
def login():
    redirect_uri = url_for("authorize", _external=True)
    return okta.authorize_redirect(redirect_uri)

# Callback route for Okta
@app.route("/authorize")
def authorize():
    token = okta.authorize_access_token()
    user_info = okta.parse_id_token(token)
    session["user"] = user_info
    return redirect(url_for("profile"))

# Profile page
@app.route("/profile")
def profile():
    user = session.get("user")
    if user:
        return jsonify(user)
    return redirect(url_for("login"))

# Logout route
@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("home"))

# Create DB
if __name__ == "__main__":
    db.create_all()
    app.run(debug=True, port=8088)

Describe the actual results

Tell us what happens instead.
So there isn't a traceback as-such, but the userinfo endpoint, which I took from the me object in .venv/lib/python3.11/site-packages/flask_appbuilder/security/manager.py`:

        if provider == "okta":
            me = self.appbuilder.sm.oauth_remotes[provider].post("userinfo")
            data = me.json()

Gets a 404 with HTML info...

{'User-Agent': 'Authlib/1.3.2 (+https://authlib.org/)', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '0', 'Authorization': 'Bearer <I have redacted this>'}

Steps to reproduce

Start a new OAuth app in a free okta developer account
Use the python provided

python -m venv .venv
. .venv/bin/activate
pip install  Flask Flask-AppBuilder Authlib requests

I am using the following VSCode JSON to launch flask so I can interactively debug

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Flask",
            "type": "debugpy",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "main.py",
                "FLASK_DEBUG": "1"
            },
            "args": [
                "run",
                "--debug",
                "--port",
                "8088",
            ],
            "jinja": true,
            "autoStartBrowser": false,
            "justMyCode": false,
        }
    ]
}

Output from CURL (access token redacted)

curl -v -X POST -H 'Authorization: Bearer <redacted token>' 'https://dev-83615971.okta.com/oauth2/v1/userinfo' | jq


  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Host dev-83615971.okta.com:443 was resolved.
* IPv6: (none)
* IPv4: 75.2.37.199, 99.83.233.105
*   Trying 75.2.37.199:443...
* Connected to dev-83615971.okta.com (75.2.37.199) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
} [326 bytes data]
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
{ [100 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2953 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [333 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [70 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Okta, Inc.; CN=*.okta.com
*  start date: Feb 12 00:00:00 2024 GMT
*  expire date: Mar 14 23:59:59 2025 GMT
*  subjectAltName: host "dev-83615971.okta.com" matched cert's "*.okta.com"
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS RSA SHA256 2020 CA1
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://dev-83615971.okta.com/oauth2/v1/userinfo
* [HTTP/2] [1] [:method: POST]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: dev-83615971.okta.com]
* [HTTP/2] [1] [:path: /oauth2/v1/userinfo]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
* [HTTP/2] [1] [authorization: Bearer <redacted token>]
> POST /oauth2/v1/userinfo HTTP/2
> Host: dev-83615971.okta.com
> User-Agent: curl/8.7.1
> Accept: */*
> Authorization: Bearer <redacted token>
>
* Request completely sent off
< HTTP/2 200
< date: Sat, 07 Dec 2024 16:14:27 GMT
< content-type: application/json
< server: nginx
< x-okta-request-id: 4e9063947af1abd087187035fac85d39
< x-xss-protection: 0
< p3p: CP="HONK"
< set-cookie: sid="";Version=1;Path=/;Max-Age=0
< set-cookie: xids="";Version=1;Path=/;Max-Age=0
< set-cookie: autolaunch_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/
< set-cookie: activate_ca_modal_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/
< content-security-policy: default-src 'self' dev-83615971.okta.com *.oktacdn.com; connect-src 'self' dev-83615971.okta.com dev-83615971-admin.okta.com *.oktacdn.com *.mixpanel.com *.mapbox.com *.mtls.okta.com dev-83615971.kerberos.okta.com *.authenticatorlocalprod.com:8769 http://localhost:8769 http://127.0.0.1:8769 *.authenticatorlocalprod.com:65111 http://localhost:65111 http://127.0.0.1:65111 *.authenticatorlocalprod.com:65121 http://localhost:65121 http://127.0.0.1:65121 *.authenticatorlocalprod.com:65131 http://localhost:65131 http://127.0.0.1:65131 *.authenticatorlocalprod.com:65141 http://localhost:65141 http://127.0.0.1:65141 *.authenticatorlocalprod.com:65151 http://localhost:65151 http://127.0.0.1:65151 https://oinmanager.okta.com data: *.ingest.sentry.io data.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com; script-src 'unsafe-inline' 'unsafe-eval' 'self' 'report-sample' dev-83615971.okta.com *.oktacdn.com; style-src 'unsafe-inline' 'self' dev-83615971.okta.com *.oktacdn.com; frame-src 'self' dev-83615971.okta.com dev-83615971-admin.okta.com login.okta.com *.vidyard.com com-okta-authenticator:; img-src 'self' dev-83615971.okta.com *.oktacdn.com *.tiles.mapbox.com *.mapbox.com *.vidyard.com data: data.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com blob:; font-src 'self' dev-83615971.okta.com data: *.oktacdn.com fonts.gstatic.com; frame-ancestors 'self'
< x-rate-limit-limit: 150
< x-rate-limit-remaining: 149
< x-rate-limit-reset: 1733588126
< cache-control: no-cache, no-store
< pragma: no-cache
< expires: 0
< set-cookie: JSESSIONID=C2C971AA4A9111CA767A05B917FB9AE2; Path=/; Secure; HttpOnly
< referrer-policy: strict-origin-when-cross-origin
< accept-ch: Sec-CH-UA-Platform-Version
< x-content-type-options: nosniff
< strict-transport-security: max-age=315360000; includeSubDomains
< x-robots-tag: noindex,nofollow
<
{ [275 bytes data]
100   275    0   275    0     0    274      0 --:--:--  0:00:01 --:--:--   275
* Connection #0 to host dev-83615971.okta.com left intact
{
  "sub": "00ui9heoya5Wtg4ry5d7",
  "name": "Lewis Cowles",
  "locale": "en_US",
  "email": "lewis+<alias>@example.com",
  "preferred_username": "lewis+<alias>@example.com",
  "given_name": "Lewis",
  "family_name": "Cowles",
  "zoneinfo": "America/Los_Angeles",
  "updated_at": 1733498547,
  "email_verified": true
}
@Lewiscowles1986
Copy link
Author

I've reported this to superset as well @chrispsheehan this is the root cause of why we couldn't login Friday. Weirdly, I can work around this by borrowing code from other lines.

            data = self.appbuilder.sm.oauth_remotes[provider].token.get('userinfo')
            if data is None:
                me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
                data = me.json()

This is a little disgusting, but it seems from the debugger that by the time code reaches this point userinfo was already successfully retrieved from the id token. Probably not a safe work-around 😆 This system was a complete pain to debug.

@Lewiscowles1986
Copy link
Author

Lewiscowles1986 commented Dec 7, 2024

Probably worth noting that the sample code I provided lets me login, but does refuse to let me see the /userinfo endpoint... Probably because of the nasty hack workaround above. Again, not a suitable workaround.

Edit: it was because I gave the lowest permissions to the user, rather than map the groups from Okta to roles 😊 By changing to default to Admin, I was able to view my own user profile... such fun

@Lewiscowles1986
Copy link
Author

I am actually a little bit mad about this now I've found a fix.

It's likely Okta's fault for not having stable URL's, but the design of this library did not make the win easy to receive at all.

@Lewiscowles1986
Copy link
Author

Semi interesting information, was that by configuring the client to say auth0 a coworker was able to have this "just work", without any edits; but the server had to remain saying it was okta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant