Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b7a1291
initial add of code from vegbank module, minor changes to get tests p…
jeanetteclark May 6, 2026
7d9c6d3
add basic factory and adapter structure
jeanetteclark May 7, 2026
bfbbe9d
rework how client is configured
jeanetteclark May 8, 2026
5acfdc0
remove login methods
jeanetteclark May 8, 2026
ab8eb44
add all the token validation methods
jeanetteclark May 8, 2026
5364038
prune out all the code we moved
jeanetteclark May 8, 2026
d16c62a
rename this file
jeanetteclark May 8, 2026
fbb156b
add example application code doc
jeanetteclark May 8, 2026
8158431
merge Rushi's diagram
jeanetteclark May 8, 2026
2deb34b
fix import
jeanetteclark May 8, 2026
39f69ae
restructure to only have one module
jeanetteclark May 8, 2026
1c61196
add self to jwts keys
jeanetteclark May 8, 2026
0c3f3e7
add some missing default params
jeanetteclark May 8, 2026
789101a
improve error handling
jeanetteclark May 8, 2026
260449e
improve error catching
jeanetteclark May 11, 2026
dcdf6bf
fix the code...
jeanetteclark May 11, 2026
07ace97
fix var name
jeanetteclark May 12, 2026
b3f6f6f
change default provider name to dataone_oidc
jeanetteclark May 12, 2026
b6eb238
change access mode param
jeanetteclark May 12, 2026
d271625
add access modes
jeanetteclark May 12, 2026
6be7143
override baseauth for fastAPI to await appropriately
jeanetteclark May 12, 2026
04112df
ruff fixes
jeanetteclark May 12, 2026
542762e
add httpx
jeanetteclark May 12, 2026
9976d90
more ruff fixes
jeanetteclark May 12, 2026
267d9fc
bring exception handling helpers into lib
jeanetteclark May 14, 2026
d7b172b
ruff fixes
jeanetteclark May 14, 2026
e593bf3
update error map
jeanetteclark May 14, 2026
5a1637f
add better error checking for token extraction
jeanetteclark May 14, 2026
4ea10b1
add login, refresh, auth methods for both adapters
jeanetteclark May 14, 2026
54441ed
add request to fastAPI calls
jeanetteclark May 14, 2026
6cd1564
add require_scope methods
jeanetteclark May 14, 2026
546e806
need this imports
jeanetteclark May 14, 2026
c52dc73
on startup, get access mode
jeanetteclark May 14, 2026
75e0690
add docs everywhere
jeanetteclark May 15, 2026
4d64c98
make code a little drier, and prefix some internal methods with _
jeanetteclark May 15, 2026
7d9036f
add token structure and claims decoding tests
jeanetteclark May 15, 2026
7607771
convert from jose to joserfc
jeanetteclark May 15, 2026
a1a7817
update deps
jeanetteclark May 15, 2026
dfe6465
update docs
jeanetteclark May 15, 2026
c895b06
add require_token decorator/depends
jeanetteclark May 15, 2026
ba8026e
fix up methods in fn sigs
jeanetteclark May 15, 2026
bf2bd54
make access mode call consistent with the rest
jeanetteclark May 15, 2026
37f7c9f
add require_token to docs
jeanetteclark May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 105 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- Contact us: support@dataone.org
- [DataONE discussions](https://github.com/DataONEorg/dataone/discussions)

*Product overview goes here.* Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
The `dataone-auth` module provides a framework-agnostic OpenID Connect (OIDC) authentication adapter for Python applications. Using `joserfc` and `Authlib`, its core purpose is to handle JSON Web Token (JWT) validation so that DataONE Python applications can seamlessly integrate OIDC authentication, regardless of what framework that application uses. Currently, `dataone-auth` supports both Flask and FastAPI. To integrate `dataone-auth` into an existing app, the application only needs to create an auth client (`create_client`), use the `login`, `authorize`, `refresh` methods on the corresponding endpoints, and utilize the `require_scope` helper either as a decorator or `Depends` function to protect secure endpoints. For more detail on usage, see the examples below.

DataONE creates open source, community projects. We [welcome contributions](./CONTRIBUTING.md) in many forms, including code, graphics, documentation, bug reports, testing, etc. Use the [DataONE discussions](https://github.com/DataONEorg/dataone/discussions) to discuss these contributions with us.

Expand Down Expand Up @@ -37,15 +37,115 @@ To run the code formatter and linter, use Ruff:

- `uv run ruff check .`

## Usage Example
## Usage Examples

To view more details about the Public API - see interface documentation
### Flask

Below is a minimal example for a Flask application. For the Flask implementation, applying `ProxyFix` is recommended to ensure correct redirect URIs when the app is running behind a reverse proxy or load balancer. Following standard Flask extension patterns, the `auth_client` must be explicitly bound to the application using `init_app()`. Once initialized, protect any endpoint by stacking the `@auth_client.require_scope(...)` decorator below the route definition. This automatically intercepts the Bearer token, validates the OIDC claims against the provider's JWKS, and injects the resulting claims dictionary into the view function. Note that routes can also use `@auth_client.require_token(...)` if checking scopes is not necessary. Optionally, HTTP methods can be passed to either decorator to specify auth requirements based on method if necessary.

```python
import dataone.auth
import json
import os
from flask import Flask, jsonify, request, url_for
from werkzeug.middleware.proxy_fix import ProxyFix
from dataone.auth import AuthFactory, load_client_secrets

# --- Constants & Logging ---
ACCESS_MODE_AUTHENTICATED = "authenticated"
scopes = ["ogdc:admin"]

# --- App Initialization ---
app = Flask(__name__)
app.config.update({"SECRET_KEY": os.getenv("FLASK_SECRET_KEY", os.urandom(32).hex())})

if not isinstance(app.wsgi_app, ProxyFix):
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

# --- Auth Setup ---
secrets = load_client_secrets()
auth_client = AuthFactory.create_client("flask", secrets, scopes)
auth_client.init_app(app)
app.extensions['dataone_auth'] = auth_client

# --- Routes ---
@app.route("/login")
def login():
return auth_client.login(redirect_uri=url_for("authorize", _external=True))

@app.route("/authorize")
def authorize():
return auth_client.authorize()

@app.route("/refresh", methods=["POST"])
def refresh_token():
return auth_client.refresh(request_json=request.get_json(silent=True))

@app.route("/profile", methods=["GET"])
@auth_client.require_scope("ogdc:admin")
def profile(claims):
"""Protected resource endpoint requiring 'ogdc:admin' scope."""
return jsonify({
"message": f"Authorization succeeded, {claims.get('name', 'User')}",
"claims": claims # The claims object is already a dictionary!
}), 200

# --- Execution ---
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int("4000"), debug=True)
```

# Example code here...
### FastAPI

Below is a minimal example for a FastAPI application. Unlike Flask, FastAPI doesn't require an `init_app` step; the `auth_client` is ready to use immediately upon creation. Note that `SessionMiddleware` must be added to the app to handle the OIDC state and nonce during the browser-based login and authorization flow. For the API endpoints, the heavy lifting happens within the `Depends(auth_client.require_scope(...))` dependency, which automatically intercepts the Bearer token, validates the OIDC claims against the provider's JWKS, and injects the ready-to-use claims dictionary into the route handler. Note that routes can also use `Depends(auth_client.require_token(...))` if checking scopes is not necessary. Optionally, HTTP methods can be passed to either `Depends` to specify auth requirements based on method if necessary.

```python
import os
from fastapi import Depends, FastAPI, Request
from fastapi.security import HTTPBearer
from starlette.middleware.sessions import SessionMiddleware
from dataone.auth import AuthFactory, load_client_secrets

# --- Constants & Logging ---
ACCESS_MODE_AUTHENTICATED = "authenticated"
scopes = ["ogdc:admin"]

# --- App Initialization ---
app = FastAPI(title="DataONE OIDC API")
app.add_middleware(
SessionMiddleware,
secret_key=os.getenv("SECRET_KEY", os.urandom(32).hex())
)

# --- Auth Setup ---
secrets = load_client_secrets()
auth_client = AuthFactory.create_client("fastapi", secrets, scopes)

security = HTTPBearer()
# --- Routes ---
@app.get("/login")
async def login(request: Request):
return await auth_client.login(request, redirect_uri=str(request.url_for("authorize")))

@app.get("/authorize")
async def authorize(request: Request):
return await auth_client.authorize(request)

@app.post("/refresh")
async def refresh(request: Request):
return await auth_client.refresh(await request.json())

@app.get("/profile")
async def profile(claims: dict = Depends(auth_client.require_scope("ogdc:admin"))):
"""Protected resource endpoint."""
return {
"message": f"Authorization succeeded, {claims.get('name', 'User')}",
"claims": claims
}

# --- Execution ---
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=4000, proxy_headers=True, forwarded_allow_ips="*")
```

## License
Expand Down
22 changes: 21 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,31 @@ authors = [
{ name = "Matthew B. Jones", email = "jones@nceas.ucsb.edu" }
]
requires-python = ">=3.13"
dependencies = []
dependencies = [
"authlib>=1.7.2",
"flask>=3.1.3",
"httpx>=0.28.1",
"joserfc>=1.6.5",
"requests>=2.33.1",
"werkzeug>=3.1.8",
]

[project.scripts]
dataone = "dataone:main"

[project.optional-dependencies]
flask = [
"flask>=3.1.3",
]
fastapi = [
"fastapi>=0.136.1",
"httpx>=0.28.1",
"starlette>=1.0.0",
]
starlette = [
"httpx>=0.28.1",
]

[tool.hatch.build.targets.wheel]
packages = ["src/dataone"]

Expand Down
Loading