Skip to content

Okta login is broken #2291

@Lewiscowles1986

Description

@Lewiscowles1986

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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions