Skip to content

Commit e5ae2b8

Browse files
authored
Merge branch 'master' into update_azure_oauth
2 parents 3d94511 + 5099b21 commit e5ae2b8

File tree

36 files changed

+4271
-38
lines changed

36 files changed

+4271
-38
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
runs-on: ubuntu-22.04
4242
strategy:
4343
matrix:
44-
python-version: ["3.9", "3.10", "3.11", "3.12"]
44+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
4545
env:
4646
SQLALCHEMY_DATABASE_URI:
4747
postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app
@@ -141,7 +141,7 @@ jobs:
141141
runs-on: ubuntu-22.04
142142
strategy:
143143
matrix:
144-
python-version: ["3.10"]
144+
python-version: ["3.10", "3.13"]
145145
env:
146146
SQLALCHEMY_DATABASE_URI: |
147147
mysql+mysqldb://mysqluser:mysqluserpassword@127.0.0.1:13306/app?charset=utf8mb4&binary_prefix=true
@@ -192,7 +192,7 @@ jobs:
192192
runs-on: ubuntu-22.04
193193
strategy:
194194
matrix:
195-
python-version: ["3.10"]
195+
python-version: ["3.10", "3.13"]
196196
env:
197197
SQLALCHEMY_DATABASE_URI: |
198198
mssql+pyodbc://sa:Password_123@localhost:11433/master?driver=FreeTDS

CHANGELOG.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
Flask-AppBuilder ChangeLog
22
==========================
33

4+
Improvements and Bug fixes on 5.2.0
5+
-----------------------------------
6+
7+
- feat: add API key authentication support (#2431) [Amin Ghadersohi]
8+
- ci: add py3.13 to the test matrix (#2419) [jnahmias]
9+
- feat: Add security model signals for User, Role, and Group CRUD operations (#2432) [Daniel Vaz Gaspar]
10+
11+
Improvements and Bug fixes on 5.1.0
12+
-----------------------------------
13+
14+
- feat: SAML Authentication (#2426) [Daniel Vaz Gaspar]
15+
- fix: update user.changed_on when roles are modified (#2423) [alok kumar priyadarshi]
16+
417
Improvements and Bug fixes on 5.0.2
518
-----------------------------------
619

docs/rest_api.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,28 @@ methods::
517517
class ExampleApi(BaseApi):
518518
base_permissions = ['can_private']
519519

520+
API Key Authentication
521+
~~~~~~~~~~~~~~~~~~~~~~
522+
523+
In addition to JWT tokens, FAB supports API key authentication. API keys are long-lived
524+
Bearer tokens useful for service-to-service communication or automation scripts.
525+
526+
To enable API key authentication, set ``FAB_API_KEY_ENABLED = True`` in your config.
527+
528+
Once enabled, you can use an API key in the same way as a JWT token::
529+
530+
$ curl http://localhost:8080/api/v1/example/private \
531+
-H "Authorization: Bearer sst_<YOUR_API_KEY>"
532+
533+
API keys are distinguished from JWT tokens by their prefix (default: ``sst_``). When the
534+
``protect()`` decorator receives a request with an API key:
535+
536+
- If the key is **invalid**, the endpoint returns HTTP **401 Unauthorized**.
537+
- If the key is valid but the user **lacks permission**, the endpoint returns HTTP **403 Forbidden**.
538+
- If the key is valid and the user **has permission**, the request proceeds normally.
539+
540+
The OpenAPI spec for protected endpoints lists both ``jwt`` and ``api_key`` as valid security
541+
schemes, so clients can choose either authentication method.
520542

521543
You can create an alternate JWT user loader, this can be useful if you want
522544
to use an external Authentication provider and map the JWT identity to your

docs/security.rst

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Supported Authentication Types
1616
It's the web server responsibility to authenticate the user, useful for intranet sites, when the server (Apache, Nginx)
1717
is configured to use kerberos, no need for the user to login with username and password on F.A.B.
1818
:OAUTH: Authentication using OAUTH (v1 or v2). You need to install authlib.
19+
:SAML: Authentication using SAML 2.0 (e.g., Microsoft Entra ID, Okta, OneLogin). You need to install python3-saml.
1920

2021
.. note::
2122
**Deprecated Authentication Types (Removed in Flask-AppBuilder 5.0+)**
@@ -31,15 +32,16 @@ The session is preserved and encrypted using Flask-Login.
3132
Authentication Methods
3233
----------------------
3334

34-
You can choose one from 4 authentication methods. Configure the method to be used
35+
You can choose one from 5 authentication methods. Configure the method to be used
3536
on the **config.py** (when using the create-app, or following the proposed app structure). First the
3637
configuration imports the constants for the authentication methods::
3738

3839
from flask_appbuilder.security.manager import (
3940
AUTH_DB,
4041
AUTH_LDAP,
4142
AUTH_OAUTH,
42-
AUTH_REMOTE_USER
43+
AUTH_REMOTE_USER,
44+
AUTH_SAML,
4345
)
4446

4547
Next you will use the **AUTH_TYPE** key to choose the type::
@@ -438,6 +440,138 @@ Therefore, you can send tweets, post on the users Facebook, retrieve the user's
438440
Take a look at the `example <https://github.com/dpgaspar/Flask-AppBuilder/tree/master/examples/oauth>`_
439441
to get an idea of a simple use for this.
440442

443+
Authentication: SAML
444+
--------------------
445+
446+
This method will authenticate users via SAML 2.0 identity providers such as
447+
Microsoft Entra ID (formerly Azure AD), Okta, OneLogin, etc.
448+
449+
.. note:: To use SAML you need to install `python3-saml <https://github.com/SAML-Toolkits/python3-saml>`_:
450+
``pip install flask-appbuilder[saml]``
451+
452+
Configure your SAML providers and SP settings in **config.py**::
453+
454+
AUTH_TYPE = AUTH_SAML
455+
456+
# registration configs
457+
AUTH_USER_REGISTRATION = True
458+
AUTH_USER_REGISTRATION_ROLE = "Public"
459+
460+
# Sync roles at login from SAML assertion
461+
AUTH_ROLES_SYNC_AT_LOGIN = True
462+
463+
# Map SAML group names to FAB roles
464+
AUTH_ROLES_MAPPING = {
465+
"admins": ["Admin"],
466+
"users": ["Public"],
467+
}
468+
469+
# SAML Identity Providers
470+
SAML_PROVIDERS = [
471+
{
472+
"name": "entra_id",
473+
"icon": "fa-microsoft",
474+
"idp": {
475+
"entityId": "https://sts.windows.net/<TENANT_ID>/",
476+
"singleSignOnService": {
477+
"url": "https://login.microsoftonline.com/<TENANT_ID>/saml2",
478+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
479+
},
480+
"singleLogoutService": {
481+
"url": "https://login.microsoftonline.com/<TENANT_ID>/saml2",
482+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
483+
},
484+
"x509cert": "<IDP_CERTIFICATE_BASE64>",
485+
},
486+
"attribute_mapping": {
487+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "email",
488+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": "first_name",
489+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname": "last_name",
490+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "username",
491+
"http://schemas.microsoft.com/ws/2008/06/identity/claims/groups": "role_keys",
492+
},
493+
},
494+
]
495+
496+
# Global SAML Service Provider configuration
497+
SAML_CONFIG = {
498+
"strict": True,
499+
"debug": False,
500+
"sp": {
501+
"entityId": "https://myapp.example.com/saml/metadata/",
502+
"assertionConsumerService": {
503+
"url": "https://myapp.example.com/saml/acs/",
504+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
505+
},
506+
"singleLogoutService": {
507+
"url": "https://myapp.example.com/saml/slo/",
508+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
509+
},
510+
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
511+
"x509cert": "",
512+
# "privateKey": "",
513+
},
514+
"security": {
515+
"nameIdEncrypted": False,
516+
"authnRequestsSigned": False,
517+
"logoutRequestSigned": False,
518+
"logoutResponseSigned": False,
519+
"signMetadata": False,
520+
"wantMessagesSigned": False,
521+
"wantAssertionsSigned": True,
522+
"wantAssertionsEncrypted": False,
523+
"wantNameId": True,
524+
"wantNameIdEncrypted": False,
525+
"wantAttributeStatement": True,
526+
},
527+
}
528+
529+
Each SAML provider entry has the following keys:
530+
531+
:name: A unique name for the identity provider.
532+
:icon: A Font Awesome icon class for the login button.
533+
:idp: The IdP SAML metadata (entityId, SSO/SLO URLs, and signing certificate).
534+
:attribute_mapping: Maps SAML assertion attribute names (left) to FAB user fields (right).
535+
Supported FAB fields: ``username``, ``email``, ``first_name``, ``last_name``, ``role_keys``.
536+
537+
The ``SAML_CONFIG`` dict holds the global Service Provider settings. The ``sp`` section defines
538+
your application's SAML endpoints. These URLs must match what you configure on the IdP side.
539+
540+
SAML Endpoints
541+
~~~~~~~~~~~~~~
542+
543+
The following endpoints are automatically registered:
544+
545+
- ``/login/`` — Login page with IdP selection (or auto-redirect for single IdP)
546+
- ``/login/<idp>`` — Initiate SSO with a specific IdP
547+
- ``/saml/acs/`` — Assertion Consumer Service (receives SAML responses)
548+
- ``/saml/slo/`` — Single Logout endpoint
549+
- ``/saml/metadata/`` — SP metadata XML (configure this URL on your IdP)
550+
551+
SAML Role Mapping
552+
~~~~~~~~~~~~~~~~~
553+
554+
You can map SAML group claims to FAB roles, just like with OAuth and LDAP::
555+
556+
AUTH_ROLES_MAPPING = {
557+
"admins": ["Admin"],
558+
"users": ["User"],
559+
}
560+
561+
AUTH_ROLES_SYNC_AT_LOGIN = True
562+
563+
PERMANENT_SESSION_LIFETIME = 1800
564+
565+
The ``role_keys`` field in ``attribute_mapping`` defines which SAML attribute contains the
566+
user's group memberships.
567+
568+
You can also use JMESPath expressions for dynamic role assignment::
569+
570+
AUTH_USER_REGISTRATION_ROLE_JMESPATH = "role_keys[0]"
571+
572+
Take a look at the `SAML example <https://github.com/dpgaspar/Flask-AppBuilder/tree/master/examples/saml>`_
573+
574+
441575
Authentication: Rate limiting
442576
-----------------------------
443577

@@ -448,6 +582,47 @@ The rate can be changed by adjusting ``AUTH_RATE_LIMIT`` to, for example, ``1 pe
448582
at the `documentation <https://flask-limiter.readthedocs.io/en/stable/>`_ of Flask-Limiter for more options and
449583
examples.
450584

585+
Authentication: API Keys
586+
------------------------
587+
588+
FAB supports API key authentication as an alternative to JWT tokens. API keys are long-lived
589+
credentials that can be used for service-to-service communication or automation.
590+
591+
**Enabling API Key Authentication**
592+
593+
Set the following in your config::
594+
595+
FAB_API_KEY_ENABLED = True
596+
597+
**Creating API Keys**
598+
599+
API keys are managed through the ``SecurityManager``. You can create keys programmatically::
600+
601+
from flask import current_app
602+
603+
sm = current_app.appbuilder.sm
604+
api_key = sm.create_api_key(user=user, name="my-service-key")
605+
606+
The returned key string should be stored securely -- it cannot be retrieved again after creation.
607+
608+
**Using API Keys**
609+
610+
Pass the API key as a Bearer token in the ``Authorization`` header::
611+
612+
$ curl http://localhost:8080/api/v1/example/private \
613+
-H "Authorization: Bearer sst_<YOUR_API_KEY>"
614+
615+
API keys use the same permission system as regular users. The key inherits the roles and
616+
permissions of the user it belongs to.
617+
618+
**Configuration Options**
619+
620+
The following configuration options are available:
621+
622+
- ``FAB_API_KEY_ENABLED`` -- Set to ``True`` to enable API key authentication (default: ``False``).
623+
- ``FAB_API_KEY_PREFIXES`` -- List of prefixes that identify API keys vs JWT tokens
624+
(default: ``["sst_"]``).
625+
451626
Role based
452627
----------
453628

@@ -849,6 +1024,9 @@ F.A.B. uses a different user view for each authentication method
8491024

8501025
:UserDBModelView: For database auth method
8511026
:UserLDAPModelView: For LDAP auth method
1027+
:UserOAuthModelView: For OAuth auth method
1028+
:UserRemoteUserModelView: For Remote User auth method
1029+
:UserSAMLModelView: For SAML auth method
8521030

8531031
You can extend or create from scratch your own, and then tell F.A.B. to use them instead, by overriding their
8541032
correspondent lower case properties on **SecurityManager** (just like on the given example).
@@ -882,6 +1060,7 @@ If you're using:
8821060
:AUTH_LDAP: Extend UserLDAPModelView
8831061
:AUTH_REMOTE_USER: Extend UserRemoteUserModelView
8841062
:AUTH_OAUTH: Extend UserOAuthModelView
1063+
:AUTH_SAML: Extend UserSAMLModelView
8851064

8861065
So using AUTH_DB::
8871066

@@ -956,6 +1135,8 @@ Note that this is for AUTH_DB, so if you're using:
9561135
:AUTH_DB: Override userdbmodelview
9571136
:AUTH_LDAP: Override userldapmodelview
9581137
:AUTH_REMOTE_USER: Override userremoteusermodelview
1138+
:AUTH_OAUTH: Override useroauthmodelview
1139+
:AUTH_SAML: Override usersamlmodelview
9591140

9601141
Finally (as shown on the previous example) tell F.A.B. to use your SecurityManager class, so when initializing
9611142
**AppBuilder** (on __init__.py)::

examples/saml/app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Example Flask-AppBuilder application with SAML / Entra ID authentication."""
2+
3+
from app import create_app
4+
5+
app = create_app()
6+
7+
if __name__ == "__main__":
8+
app.run(host="0.0.0.0", port=5000, debug=True)

examples/saml/app/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import logging
2+
3+
from flask import Flask
4+
5+
from .extensions import appbuilder, db
6+
7+
8+
logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s")
9+
logging.getLogger().setLevel(logging.INFO)
10+
11+
12+
def create_app() -> Flask:
13+
app = Flask(__name__)
14+
app.config.from_object("config")
15+
with app.app_context():
16+
db.init_app(app)
17+
appbuilder.init_app(app, db.session)
18+
db.create_all()
19+
return app

examples/saml/app/extensions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask_appbuilder import AppBuilder
2+
from flask_appbuilder.utils.legacy import get_sqla_class
3+
4+
db = get_sqla_class()()
5+
appbuilder = AppBuilder()

0 commit comments

Comments
 (0)