| page_id | 2b425ccb-d328-41ff-afb2-016019f46839 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| title | Python SDK | |||||||||
| description | Complete guide for Python SDK including Flask and FastAPI integration, OAuth configuration, environment variables, and session management for Python 3.9+ applications. | |||||||||
| sidebar |
|
|||||||||
| head |
|
|||||||||
| topics |
|
|||||||||
| sdk |
|
|||||||||
| languages |
|
|||||||||
| audience | developers | |||||||||
| complexity | intermediate | |||||||||
| keywords |
|
|||||||||
| updated | 2026-01-23 | |||||||||
| featured | false | |||||||||
| deprecated | false | |||||||||
| ai_summary | Complete guide for Python SDK including Flask and FastAPI integration, OAuth configuration, environment variables, and session management for Python 3.9+ applications. |
The Kinde Python SDK allows developers to quickly and securely integrate a new or an existing Python application into the Kinde platform. The SDK supports both Flask and FastAPI frameworks through a single unified interface.
If you are using a previous version of Python, you may need to refer to the previous v1 SDK.
For new projects, you can find our Starter Kit on GitHub.
If you're migrating from an older version of the SDK or switching between client types, follow these recommendations:
The current SDK (v2+) provides a more flexible architecture with multiple client types:
Old v1 SDK code
from kinde_sdk.kinde_api_client import KindeApiClient
kinde_client = KindeApiClient(**kinde_api_client_params)
login_url = kinde_client.get_login_url()New SDK code (OAuth client)
from kinde_sdk.auth.oauth import OAuth
import asyncio
oauth = OAuth(framework="flask", app=app)
login_url = asyncio.get_event_loop().run_until_complete(oauth.login())- If you're using Flask/FastAPI: Continue using
OAuthclient - If you're building async-only applications: Migrate to
AsyncOAuth - If you need flexibility: Consider
SmartOAuthfor context-aware behavior
- Update imports: Change from
kinde_sdk.kinde_api_clientto the appropriate client type - Update initialization: Use the new client initialization pattern
- Update method calls: Most methods are now async - use
awaitorasyncio.run_until_complete() - Update error handling: Use new exception types from
kinde_sdk.exceptions - Test thoroughly: Verify all authentication flows work correctly
| Scenario | Recommended Client | Reason |
|---|---|---|
| Flask application | OAuth |
Framework integration, session management |
| FastAPI application | OAuth or AsyncOAuth |
Both work well, choose based on preference |
| Pure async app | AsyncOAuth |
Native async support, better performance |
| Serverless/Lambda | AsyncOAuth |
No framework dependencies, async-friendly |
| Library/Utility | SmartOAuth |
Works in both sync and async contexts |
| Mixed codebase | SmartOAuth |
Context-aware behavior |
For more detailed migration instructions, see our GitHub migration guide.
Install PIP and then execute the following command:
pip install kinde-python-sdkThe Kinde Python SDK uses environment variables for configuration. Here are all the supported variables:
KINDE_CLIENT_ID- Your application's client ID from KindeKINDE_CLIENT_SECRET- Your application's client secret from KindeKINDE_REDIRECT_URI- The callback URL where Kinde will redirect after authenticationKINDE_HOST- Your Kinde domain (e.g.,https://yourdomain.kinde.com)KINDE_ISSUER_URL- Your Kinde issuer URL (typically same as KINDE_HOST)GRANT_TYPE- The OAuth grant type to use (e.g.,AUTHORIZATION_CODE_WITH_PKCE)
KINDE_AUDIENCE- The intended recipient of the access token (for API access)KINDE_CALLBACK_URL- Alternative name for KINDE_REDIRECT_URILOGOUT_REDIRECT_URL- Where users are redirected after logoutSITE_HOST- Your application's host (default:127.0.0.1)SITE_PORT- Your application's port (default:5000)SITE_URL- Your application's base URLCODE_VERIFIER- Required for PKCE flow (auto-generated if not provided)
Session management variables (core SDK features):
SECRET_KEY- Used for session management and token securitySESSION_TYPE- Session storage type (e.g.,filesystem)SESSION_PERMANENT- Whether sessions are permanent (default:False)
Application configuration:
TEMPLATES_AUTO_RELOAD- Whether to auto-reload templates (default:True)
Management API variables (only needed if using Management API features):
MGMT_API_CLIENT_ID- Management API client IDMGMT_API_CLIENT_SECRET- Management API client secret
Example .env file:
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret
KINDE_REDIRECT_URI=http://localhost:5000/api/auth/kinde_callback
KINDE_HOST=https://yourdomain.kinde.com
KINDE_ISSUER_URL=https://yourdomain.kinde.com
GRANT_TYPE=AUTHORIZATION_CODE_WITH_PKCE
SITE_HOST=localhost
SITE_PORT=5000
SITE_URL=http://localhost:5000
LOGOUT_REDIRECT_URL=http://localhost:8000
SECRET_KEY=your_secret_key
SESSION_TYPE=filesystem
SESSION_PERMANENT=False
TEMPLATES_AUTO_RELOAD=True- In Kinde, go to Settings > Applications > [Your app] > View details.
- Add your callback URLs in the relevant fields. For example:
- Allowed callback URLs (also known as redirect URIs) - for example,
http://localhost:8000/callback - Allowed logout redirect URLs - for example,
http://localhost:8000
- Allowed callback URLs (also known as redirect URIs) - for example,
- Select Save.
Kinde comes with a production environment, but you can set up other environments if you want to. Note that each environment needs to be set up independently, so you need to use the Environment subdomain in the code block above for those new environments.
The Kinde Python SDK provides three client types to suit different application needs:
Use OAuth client when:
- You're using Flask or FastAPI frameworks
- You want framework-specific integrations (automatic route registration, session management)
- Your application follows traditional request-response patterns
- You need synchronous operations with async support via framework adapters
Use AsyncOAuth client when:
- You're building a native async application (e.g., pure async Python, async web frameworks)
- You want direct async/await support without framework abstractions
- You're using async libraries that don't fit traditional framework patterns
- You need maximum performance with async I/O operations
Use SmartOAuth client when:
- Your application uses both sync and async contexts
- You want context-aware behavior (automatically sync or async based on usage)
- You're building libraries or utilities that need to work in various contexts
- You need flexibility to work with both sync and async code
| Feature | OAuth | AsyncOAuth | SmartOAuth |
|---|---|---|---|
| Framework support | Flask, FastAPI | Any async framework | Any context |
| Sync operations | ✅ | ❌ | ✅ (context-aware) |
| Async operations | ✅ (via adapters) | ✅ (native) | ✅ (native) |
| Route registration | ✅ Automatic | ❌ Manual | ❌ Manual |
| Session management | ✅ Built-in | ||
| Best for | Framework apps | Pure async apps | Mixed contexts |
The OAuth client is automatically configured based on the framework you're using. Simply import the OAuth class from the auth module and create an instance:
from kinde_sdk.auth.oauth import OAuth
# For Flask applications
from flask import Flask
app = Flask(__name__)
oauth = OAuth(
framework="flask",
app=app # optional: pass your Flask app instance
)
# For FastAPI applications
from fastapi import FastAPI
app = FastAPI()
oauth = OAuth(
framework="fastapi",
app=app # optional: pass your FastAPI app instance
)The SDK will automatically detect and configure the appropriate framework implementation based on the framework parameter and app instance you provide.
The AsyncOAuth client is designed for native async applications. It provides direct async/await support without framework abstractions:
from kinde_sdk.auth.async_oauth import AsyncOAuth
# Initialize AsyncOAuth client
oauth = AsyncOAuth()
# All methods are async
login_url = await oauth.login()
register_url = await oauth.register()Key features:
- Native async/await support
- No framework dependencies
- Direct control over async operations
- Ideal for pure async applications and serverless functions
The SmartOAuth client automatically adapts to sync or async contexts:
from kinde_sdk.auth.smart_oauth import SmartOAuth
# Initialize SmartOAuth client
oauth = SmartOAuth()
# Works in async context
async def async_function():
login_url = await oauth.login() # Returns coroutine
# Works in sync context
def sync_function():
login_url = oauth.login() # Returns URL directlyKey features:
- Context-aware behavior (sync/async)
- Works in both sync and async code
- Flexible usage patterns
- Ideal for libraries and utilities
For serverless functions (AWS Lambda, Google Cloud Functions, Azure Functions), you can use the SDK in standalone mode without framework dependencies.
import json
from kinde_sdk.auth.async_oauth import AsyncOAuth
# Initialize client (can be outside handler for reuse)
oauth = AsyncOAuth()
async def lambda_handler(event, context):
"""AWS Lambda handler for authentication."""
try:
# Handle login
if event.get('path') == '/login':
login_url = await oauth.login()
return {
'statusCode': 302,
'headers': {'Location': login_url}
}
# Handle callback
if event.get('path') == '/callback':
code = event.get('queryStringParameters', {}).get('code')
state = event.get('queryStringParameters', {}).get('state')
if not code:
return {
'statusCode': 400,
'body': json.dumps({'error': 'No code provided'})
}
result = await oauth.handle_redirect(code, state)
return {
'statusCode': 200,
'body': json.dumps({'message': 'Authenticated successfully'})
}
# Check authentication
if event.get('path') == '/user':
if await oauth.is_authenticated(event):
user_info = await oauth.get_user_info(event)
return {
'statusCode': 200,
'body': json.dumps(user_info)
}
else:
login_url = await oauth.login()
return {
'statusCode': 302,
'headers': {'Location': login_url}
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps({'error': str(e)})
}from flask import Request
from kinde_sdk.auth.async_oauth import AsyncOAuth
import asyncio
oauth = AsyncOAuth()
def cloud_function_handler(request: Request):
"""Google Cloud Function handler."""
async def handle_request():
if request.path == '/login':
url = await oauth.login()
return redirect(url, code=302)
if request.path == '/callback':
code = request.args.get('code')
state = request.args.get('state')
await oauth.handle_redirect(code, state)
return {'message': 'Authenticated'}, 200
return asyncio.run(handle_request())In serverless environments, you'll need to implement custom token storage:
from kinde_sdk.auth.async_oauth import AsyncOAuth
class ServerlessTokenStorage:
"""Custom token storage for serverless environments."""
def __init__(self, storage_backend): # e.g., Redis, DynamoDB, etc.
self.storage = storage_backend
async def store_tokens(self, user_id: str, tokens: dict):
await self.storage.set(f"tokens:{user_id}", tokens, ttl=3600)
async def get_tokens(self, user_id: str) -> dict:
return await self.storage.get(f"tokens:{user_id}")
async def clear_tokens(self, user_id: str):
await self.storage.delete(f"tokens:{user_id}")
# Usage
storage = ServerlessTokenStorage(redis_client)
oauth = AsyncOAuth(token_storage=storage)The authentication flow consists of several steps. Here's a comprehensive guide to implementing authentication with the Kinde Python SDK.
Choose the appropriate client based on your application type (see Client Selection Guide).
The Kinde client provides methods for easy sign in and sign up. You can add buttons in your HTML as follows:
<div class="navigation">
<a href="{{ url_for('login') }}" type="button">Sign in</a>
<a href="{{ url_for('register') }}" type="button">Sign up</a>
</div>The framework wrapper can automatically register all necessary routes. For Flask:
from kinde_sdk.auth.oauth import OAuth
from flask import Flask
app = Flask(__name__)
oauth = OAuth(
framework="flask",
app=app
)For FastAPI:
from kinde_sdk.auth.oauth import OAuth
from fastapi import FastAPI
app = FastAPI()
oauth = OAuth(
framework="fastapi",
app=app
)Flask applications use the OAuth client with framework support, or AsyncOAuth for async patterns.
Using OAuth client (recommended for Flask):
from flask import Flask, request, session, redirect, url_for
from kinde_sdk.auth.oauth import OAuth
import asyncio
app = Flask(__name__)
app.secret_key = 'your-secret-key'
oauth = OAuth(
framework="flask",
app=app
)
@app.route('/login')
def login():
"""Redirect to Kinde login page."""
loop = asyncio.get_event_loop()
login_url = loop.run_until_complete(oauth.login())
return redirect(login_url)
@app.route('/register')
def register():
"""Redirect to Kinde registration page."""
loop = asyncio.get_event_loop()
register_url = loop.run_until_complete(oauth.register())
return redirect(register_url)
@app.route('/callback')
def callback():
"""Handle the OAuth callback from Kinde."""
try:
code = request.args.get('code')
state = request.args.get('state')
if not code:
return "Authentication failed: No code provided", 400
user_id = session.get('user_id') or str(uuid.uuid4())
loop = asyncio.get_event_loop()
result = loop.run_until_complete(oauth.handle_redirect(code, user_id, state))
session['user_id'] = user_id
return redirect(url_for('index'))
except Exception as e:
return f"Authentication failed: {str(e)}", 400
@app.route('/logout')
def logout():
"""Logout the user and redirect to Kinde logout page."""
user_id = session.get('user_id')
session.clear()
loop = asyncio.get_event_loop()
logout_url = loop.run_until_complete(oauth.logout(user_id))
return redirect(logout_url)
@app.route('/user')
def get_user():
"""Get the current user's information."""
try:
if not oauth.is_authenticated(request):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
login_url = loop.run_until_complete(oauth.login())
return redirect(login_url)
finally:
loop.close()
return oauth.get_user_info(request)
except Exception as e:
return f"Failed to get user info: {str(e)}", 400FastAPI natively supports async/await, making it ideal for async operations.
Using OAuth client with FastAPI:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(
framework="fastapi",
app=app
)
@app.get("/login")
async def login(request: Request):
"""Redirect to Kinde login page."""
url = await oauth.login()
return RedirectResponse(url=url)
@app.get("/register")
async def register(request: Request):
"""Redirect to Kinde registration page."""
url = await oauth.register()
return RedirectResponse(url=url)
@app.get("/callback")
async def callback(request: Request, code: str, state: str = None):
"""Handle the OAuth callback from Kinde."""
try:
result = await oauth.handle_redirect(code, state)
return RedirectResponse(url="/")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}")
@app.get("/logout")
async def logout(request: Request):
"""Logout the user and redirect to Kinde logout page."""
await oauth.logout()
return RedirectResponse(url="/")
@app.get("/user")
async def get_user(request: Request):
"""Get the current user's information."""
if not await oauth.is_authenticated(request):
url = await oauth.login()
return RedirectResponse(url=url)
return await oauth.get_user_info(request)Using AsyncOAuth client with FastAPI (for more control):
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
from kinde_sdk.auth.async_oauth import AsyncOAuth
app = FastAPI()
oauth = AsyncOAuth()
@app.get("/login")
async def login(request: Request):
url = await oauth.login()
return RedirectResponse(url=url)
@app.get("/callback")
async def callback(request: Request, code: str, state: str = None):
try:
result = await oauth.handle_redirect(code, state)
return RedirectResponse(url="/")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))If you prefer to implement the routes manually, here's how you can do it:
For Flask:
import asyncio
from flask import Flask, request, session, redirect
from kinde_sdk.auth.oauth import OAuth
app = Flask(__name__)
oauth = OAuth(
framework="flask",
app=app
)
@app.route('/login')
def login():
"""Redirect to Kinde login page.""
loop = asyncio.get_event_loop()
login_url = loop.run_until_complete(oauth.login())
return redirect(login_url)
@app.route('/register')
def register():
"""Redirect to Kinde registration page.""
loop = asyncio.get_event_loop()
register_url = loop.run_until_complete(oauth.register())
return redirect(register_url)
@app.route('/callback')
def callback():
"""Handle the OAuth callback from Kinde.""
try:
code = request.args.get('code')
state = request.args.get('state')
if not code:
return "Authentication failed: No code provided", 400
# Generate a unique user ID for the session
user_id = session.get('user_id') or str(uuid.uuid4())
# Use OAuth's handle_redirect method to process the callback
loop = asyncio.get_event_loop()
result = loop.run_until_complete(oauth.handle_redirect(code, user_id, state))
# Store user ID in session
session['user_id'] = user_id
return redirect('/')
except Exception as e:
return f"Authentication failed: {str(e)}", 400
@app.route('/logout')
def logout():
"""Logout the user and redirect to Kinde logout page.""
user_id = session.get('user_id')
session.clear()
loop = asyncio.get_event_loop()
logout_url = loop.run_until_complete(oauth.logout(user_id))
return redirect(logout_url)
@app.route('/user')
def get_user():
"""Get the current user's information.""
try:
if not oauth.is_authenticated(request):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
login_url = loop.run_until_complete(oauth.login())
return redirect(login_url)
finally:
loop.close()
return oauth.get_user_info(request)
except Exception as e:
return f"Failed to get user info: {str(e)}", 400For FastAPI:
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
@app.get("/login")
async def login(request: Request):
url = await oauth.login()
return RedirectResponse(url=url)
@app.get("/register")
async def register(request: Request):
url = await oauth.register()
return RedirectResponse(url=url)
@app.get("/callback")
async def callback(request: Request, code: str, state: Optional[str] = None):
try:
result = await oauth.handle_redirect(code, state)
return RedirectResponse(url="/")
except Exception as e:
return HTMLResponse(f"Authentication failed: {str(e)}")
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url=await oauth.logout())
@app.get("/user")
async def get_user(request: Request):
if not oauth.is_authenticated(request):
return RedirectResponse(url=await oauth.login())
return oauth.get_user_info(request)The manual implementation gives you more control over the authentication flow and allows you to add custom logic like session management, error handling, and logging. Note that Flask requires special handling of async methods using asyncio since it doesn't natively support async/await like FastAPI does.
- User initiates login/registration: User clicks login/register button
- Redirect to Kinde: Application redirects user to Kinde's authorization server
- User authenticates: User signs in/up on Kinde's hosted pages
- Callback handling: Kinde redirects back to your callback URL with authorization code
- Token exchange: SDK exchanges authorization code for access tokens
- Session creation: SDK stores tokens and creates user session
- Protected resources: User can now access protected routes and resources
The Kinde Python SDK provides a simple way to check user permissions in your application. The API supports both sync and async patterns depending on your client type.
from kinde_sdk.auth.oauth import OAuth
from flask import request
import asyncio
oauth = OAuth(framework="flask", app=app)
# Async pattern (required for OAuth client)
def check_permission_sync():
loop = asyncio.get_event_loop()
permission = loop.run_until_complete(
oauth.get_permission("create:todos", request)
)
return permission["isGranted"]
# In FastAPI (native async)
@app.get("/todos")
async def create_todo(request: Request):
permission = await oauth.get_permission("create:todos", request)
if not permission["isGranted"]:
raise HTTPException(status_code=403, detail="Permission denied")
# Create todo logic...from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
# Native async pattern
async def check_permission():
permission = await oauth.get_permission("create:todos")
if permission["isGranted"]:
print(f"User has permission in organization: {permission['orgCode']}")
return True
return False
# Get all permissions
async def get_all_permissions():
all_permissions = await oauth.get_permissions()
print(f"User belongs to organization: {all_permissions['orgCode']}")
print("User permissions:", all_permissions["permissions"])
return all_permissionsfrom kinde_sdk.auth.smart_oauth import SmartOAuth
oauth = SmartOAuth()
# Works in async context
async def async_check():
permission = await oauth.get_permission("create:todos")
return permission["isGranted"]
# Works in sync context (if available)
def sync_check():
permission = oauth.get_permission("create:todos")
return permission["isGranted"]Here's how to use permissions in your application with different client types:
Example 1: Permission check in FastAPI (OAuth client)
from fastapi import FastAPI, Request, HTTPException
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.post("/todos")
async def create_todo(request: Request, todo_data: dict):
permission = await oauth.get_permission("create:todos", request)
if not permission["isGranted"]:
raise HTTPException(status_code=403, detail="Permission denied")
# Create todo logic here...
return {"message": "Todo created"}
@app.get("/todos")
async def list_todos(request: Request):
permission = await oauth.get_permission("read:todos", request)
if not permission["isGranted"]:
raise HTTPException(status_code=403, detail="Permission denied")
# List todos logic...
return {"todos": []}Example 2: Permission check with AsyncOAuth client
from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
async def create_todo_handler(request):
permission = await oauth.get_permission("create:todos")
if not permission["isGranted"]:
return {"error": "Permission denied"}, 403
org_code = permission.get("orgCode")
# Create todo with organization context
return {"message": "Todo created", "org_code": org_code}
async def get_all_user_permissions():
all_permissions = await oauth.get_permissions()
return {
"org_code": all_permissions["orgCode"],
"permissions": all_permissions["permissions"]
}Example 3: Permission-based conditional rendering (Flask)
from flask import Flask, request, render_template
from kinde_sdk.auth.oauth import OAuth
import asyncio
app = Flask(__name__)
oauth = OAuth(framework="flask", app=app)
@app.route("/dashboard")
def dashboard():
loop = asyncio.get_event_loop()
can_create = loop.run_until_complete(
oauth.get_permission("create:todos", request)
)["isGranted"]
can_delete = loop.run_until_complete(
oauth.get_permission("delete:todos", request)
)["isGranted"]
return render_template("dashboard.html",
can_create=can_create,
can_delete=can_delete)Here are some common permission patterns you might use:
# Resource-based permissions
"create:todos
"read:todos
"update:todos
"delete:todos
# Feature-based permissions
"can:export_data
"can:manage_users
"can:view_analytics
# Organization-based permissions
"org:manage_members
"org:view_billing
"org:update_settingsFor more information about setting up permissions in Kinde, see User permissions.
The Kinde Python SDK provides a simple way to access feature flags from your application. Feature flags support both sync and async patterns.
from kinde_sdk.auth.oauth import OAuth
from flask import request
import asyncio
oauth = OAuth(framework="flask", app=app)
# Async pattern (required for OAuth client)
def get_theme_sync():
loop = asyncio.get_event_loop()
theme_flag = loop.run_until_complete(
oauth.get_flag("theme", request, default_value="light")
)
return theme_flag.value
# In FastAPI (native async)
@app.get("/settings")
async def get_settings(request: Request):
theme = await oauth.get_flag("theme", request, default_value="light")
dark_mode = await oauth.get_flag("is_dark_mode", request, default_value=False)
return {
"theme": theme.value,
"dark_mode": dark_mode.value
}from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
# Native async pattern
async def get_feature_flags():
# Get a string feature flag
theme_flag = await oauth.get_flag("theme", default_value="light")
print(f"Current theme: {theme_flag.value}")
# Get a boolean feature flag with default value
dark_mode = await oauth.get_flag("is_dark_mode", default_value=False)
if dark_mode.value:
print("Dark mode is enabled")
# Get a numeric feature flag
competitions_limit = await oauth.get_flag("competitions_limit", default_value=3)
print(f"User can create up to {competitions_limit.value} competitions")
return {
"theme": theme_flag.value,
"dark_mode": dark_mode.value,
"limit": competitions_limit.value
}To get a specific feature flag value:
Here's how to use feature flags in your application with different client types:
Example 1: Conditional feature rendering (FastAPI with OAuth)
from fastapi import FastAPI, Request
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.get("/competitions/create-button")
async def render_create_button(request: Request):
can_create = await oauth.get_flag("create_competition", request, default_value=False)
if can_create.value:
return {"html": "<button>Create Competition</button>"}
return {"html": ""}Example 2: Theme configuration (AsyncOAuth)
from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
async def get_user_theme():
theme = await oauth.get_flag("theme", default_value="light")
dark_mode = await oauth.get_flag("is_dark_mode", default_value=False)
return {
"theme": theme.value,
"is_dark_mode": dark_mode.value
}Example 3: Feature limits with validation (FastAPI)
from fastapi import FastAPI, Request, HTTPException
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.post("/competitions")
async def create_competition(request: Request, competition_data: dict):
limit_flag = await oauth.get_flag("competitions_limit", request, default_value=3)
current_count = await get_user_competition_count(request)
if current_count >= limit_flag.value:
raise HTTPException(
status_code=403,
detail=f"Competition limit reached (max: {limit_flag.value})"
)
# Create competition logic here...
return {"message": "Competition created"}Example 4: Type-safe flag access (AsyncOAuth)
from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
async def get_all_flags():
# Get all feature flags
all_flags = await oauth.get_all_flags()
result = {}
for code, flag in all_flags.items():
result[code] = {
"value": flag.value,
"type": flag.type,
"is_default": flag.is_default
}
return result
# Type-specific getters
async def get_boolean_flag(flag_name: str, default: bool = False):
return await oauth.get_boolean_flag(flag_name, default_value=default)
async def get_string_flag(flag_name: str, default: str = ""):
return await oauth.get_string_flag(flag_name, default_value=default)
async def get_integer_flag(flag_name: str, default: int = 0):
return await oauth.get_integer_flag(flag_name, default_value=default)The SDK supports the following feature flag types:
# String flags
{
"t": "s",
"v": "pink"
}
# Boolean flags
{
"t": "b",
"v": true
}
# Integer flags
{
"t": "i",
"v": 5
}# Feature Toggles
can_use_feature = await feature_flags.get_flag("enable_new_feature", default_value=False)
# User Preferences
theme = await feature_flags.get_flag("theme", default_value="light")
dark_mode = await feature_flags.get_flag("is_dark_mode", default_value=False)
# Usage Limits
max_uploads = await feature_flags.get_flag("max_uploads", default_value=10)
# A/B Testing
test_group = await feature_flags.get_flag("ab_test_group", default_value="control")The Kinde Python SDK provides a simple way to access user claims from your application. Claims support both sync and async patterns.
from kinde_sdk.auth.oauth import OAuth
from flask import request
import asyncio
oauth = OAuth(framework="flask", app=app)
# Async pattern (required for OAuth client)
def get_user_name_sync():
loop = asyncio.get_event_loop()
claim = loop.run_until_complete(
oauth.get_claim("given_name", request, token_type="id_token")
)
return claim["value"]
# In FastAPI (native async)
@app.get("/profile")
async def get_profile(request: Request):
given_name = await oauth.get_claim("given_name", request, token_type="id_token")
family_name = await oauth.get_claim("family_name", request, token_type="id_token")
email = await oauth.get_claim("email", request, token_type="id_token")
return {
"name": f"{given_name['value']} {family_name['value']}",
"email": email["value"]
}from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
# Native async pattern
async def get_claims():
# Get the audience claim from the access token
aud_claim = await oauth.get_claim("aud")
print(f"Token audience: {aud_claim['value']}")
# Get the given_name claim from the ID token
name_claim = await oauth.get_claim("given_name", token_type="id_token")
print(f"User's given name: {name_claim['value']}")
return {
"audience": aud_claim["value"],
"name": name_claim["value"]
}To get a specific claim from the user's tokens:
Here's how to use claims in your application with different client types:
Example 1: Accessing user information (FastAPI with OAuth)
from fastapi import FastAPI, Request
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.get("/profile")
async def get_user_profile(request: Request):
given_name = await oauth.get_claim("given_name", request, token_type="id_token")
family_name = await oauth.get_claim("family_name", request, token_type="id_token")
email = await oauth.get_claim("email", request, token_type="id_token")
if given_name["value"] and family_name["value"]:
return {
"name": f"{given_name['value']} {family_name['value']}",
"email": email["value"]
}
return NoneExample 2: Token validation (AsyncOAuth)
from fastapi import HTTPException
from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
async def validate_token():
aud_claim = await oauth.get_claim("aud")
if not aud_claim["value"] or "api.yourapp.com" not in aud_claim["value"]:
raise HTTPException(status_code=401, detail="Invalid token audience")
return {"message": "Access granted"}
@app.get("/api/protected")
async def protected_endpoint():
return await validate_token()Example 3: Getting all claims (AsyncOAuth)
from kinde_sdk.auth.async_oauth import AsyncOAuth
oauth = AsyncOAuth()
async def get_all_user_claims():
# Get all claims from the access token
all_claims = await oauth.get_all_claims()
access_token_data = {}
for claim_name, claim_value in all_claims.items():
access_token_data[claim_name] = claim_value
# Get all claims from the ID token
id_token_claims = await oauth.get_all_claims(token_type="id_token")
id_token_data = {}
for claim_name, claim_value in id_token_claims.items():
id_token_data[claim_name] = claim_value
return {
"access_token": access_token_data,
"id_token": id_token_data
}Example 4: Organization context (FastAPI)
from fastapi import FastAPI, Request
from kinde_sdk.auth.oauth import OAuth
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.get("/organization-info")
async def get_org_info(request: Request):
org_code = await oauth.get_claim("org_code", request)
org_name = await oauth.get_claim("org_name", request)
return {
"org_code": org_code["value"],
"org_name": org_name["value"] if org_name else None
}Here are some common claims you might want to access:
# User Information (ID Token)
"given_name
"family_name
"email
"picture
# Token Information (Access Token)
"aud" # Audience
"iss" # Issuer
"exp" # Expiration time
"iat" # Issued at time
# Organization Information
"org_code
"org_name
"org_idTo create a new organization within your application, you will need to run a similar function to below:
return app.redirect(oauth.create_org())Kinde has a unique code for every organization. You'll have to pass this code through when you register a new user or sign in to a particular organization. Example function below:
oauth.get_claim("org_code")
@app.route("/login")
def login():
return app.redirect(oauth.get_login_url())
@app.route("/register")
def register():
return app.redirect(oauth.get_register_url())Following authentication, Kinde provides a json web token (jwt) to your application. Along with the standard information we also include the org_code and the permissions for that organization (this is important as a user can belong to multiple organizations and have different permissions for each).
Example of a returned token:
{
"aud": [],
"exp": 1658475930,
"iat": 1658472329,
"iss": "https://your_subdomain.kinde.com",
"jti": "123457890",
"org_code": "org_1234",
"permissions": ["read:todos", "create:todos"],
"scp": [
"openid",
"profile",
"email",
"offline
],
"sub": "kp:123457890"
}The id_token will also contain an array of organizations that a user belongs to - this is useful if you wanted to build out an organization switcher for example.
{
...
"org_codes": ["org_1234", "org_4567"],
...
};There are two helper functions you can use to extract information:
oauth.get_organization()
oauth.get_user_organizations()For more information about how organizations work in Kinde, see Kinde organizations for developers.
The Kinde Python SDK automatically handles token and session management for your application. Once a user has successfully authenticated, the SDK manages:
- Token acquisition and storage: Automatically obtains and securely stores access tokens, ID tokens, and refresh tokens
- Token refresh: Automatically refreshes tokens when they expire
- Session management: Handles user sessions across requests
- Framework integration: Works seamlessly with Flask and FastAPI session systems
The SDK uses the session configuration from your environment variables (SECRET_KEY, SESSION_TYPE, SESSION_PERMANENT) to manage sessions appropriately for your chosen framework.
The SDK supports two types of tokens:
-
Access Token (
token_type="access_token"):- Contains authorization information
- Used for API access
- Contains permissions and organization context
- Default token type
-
ID Token (
token_type="id_token"):- Contains user identity information
- Used for user profile data
- Contains name, email, and other user details
- Must be explicitly requested using
token_type="id_token"
The SDK automatically integrates with your framework's session system:
- Flask: Uses Flask's built-in session management
- FastAPI: Integrates with FastAPI's session handling
You don't need to manually manage tokens or sessions - the SDK handles this automatically for you.
The Kinde Python SDK includes a separate Management API client for interacting with Kinde's management endpoints. This allows you to programmatically manage users, organizations, roles, permissions, feature flags, and other resources.
The Management API client is independent of the OAuth authentication clients (OAuth, AsyncOAuth, SmartOAuth). You initialize it directly with your management API credentials.
from kinde_sdk.management.management_client import ManagementClient
management = ManagementClient(
domain="your-domain.kinde.com",
client_id="your-management-client-id",
client_secret="your-management-client-secret"
)You can also load credentials from a .env file. This requires the python-dotenv package:
pip install python-dotenvCall load_dotenv() before initializing ManagementClient so the environment variables are available:
import os
from dotenv import load_dotenv
from kinde_sdk.management.management_client import ManagementClient
load_dotenv()
management = ManagementClient(
domain=os.getenv("KINDE_HOST", "").replace("https://", ""),
client_id=os.getenv("MGMT_API_CLIENT_ID"),
client_secret=os.getenv("MGMT_API_CLIENT_SECRET")
)The ManagementClient dynamically discovers all auto-generated API classes and exposes them as snake_case properties. Access endpoints through these API class properties:
User management:
# List users
users = management.users_api.get_users(page_size=10)
# Get a specific user
user = management.users_api.get_user_data(id="user_123")
# Create a new user
new_user = management.users_api.create_user(
create_user_request={
"profile": {
"given_name": "John",
"family_name": "Doe"
},
"identities": [{
"type": "email",
"details": {"email": "user@example.com"}
}]
}
)
# Update a user
management.users_api.update_user(
id="user_123",
update_user_request={"given_name": "Johnny"}
)
# Delete a user
management.users_api.delete_user(id="user_123")Other common API classes:
management.organizations_api # Organization management
management.roles_api # Role management
management.permissions_api # Permission management
management.feature_flags_api # Feature flag management
management.applications_api # Application management
management.subscribers_api # Subscriber management
management.connections_api # Connection management
management.webhooks_api # Webhook managementWhen new API classes are added to the generated module, they are automatically available on the client without any code changes.
from kinde_sdk.management.management_client import ManagementClient
management = ManagementClient(
domain="your-domain.kinde.com",
client_id="your-management-client-id",
client_secret="your-management-client-secret"
)
# List organizations
orgs = management.organizations_api.get_organizations(page_size=10)
# Get a specific organization
org = management.organizations_api.get_organization(code="org_1234")
# Create a new organization
new_org = management.organizations_api.create_organization(
create_organization_request={"name": "My Organization"}
)
# Update an organization
management.organizations_api.update_organization(
org_code="org_1234",
update_organization_request={"name": "Updated Name"}
)
# Delete an organization
management.organizations_api.delete_organization(org_code="org_1234")Note: get_organization accepts code, while update_organization and delete_organization expect org_code. If you're copy-pasting between calls, update the parameter name accordingly.
Using the Management API in a FastAPI application:
from fastapi import FastAPI, HTTPException
from kinde_sdk.management.management_client import ManagementClient
app = FastAPI()
management = ManagementClient(
domain="your-domain.kinde.com",
client_id="your-management-client-id",
client_secret="your-management-client-secret"
)
@app.get("/organizations")
def list_organizations():
orgs = management.organizations_api.get_organizations()
return orgs
@app.get("/organizations/{org_code}")
def get_organization(org_code: str):
org = management.organizations_api.get_organization(code=org_code)
return org
@app.post("/organizations")
def create_organization(name: str):
new_org = management.organizations_api.create_organization(
create_organization_request={"name": name}
)
return new_org
@app.delete("/organizations/{org_code}")
def delete_organization(org_code: str):
management.organizations_api.delete_organization(org_code=org_code)
return {"message": "Organization deleted"}The Management API methods raise exceptions for HTTP errors. Wrap calls in try/except blocks:
from kinde_sdk.management.management_client import ManagementClient
management = ManagementClient(
domain="your-domain.kinde.com",
client_id="your-management-client-id",
client_secret="your-management-client-secret"
)
try:
user = management.users_api.get_user_data(id="user_123")
except Exception as e:
print(f"Error: {e}")In a FastAPI application:
import logging
from fastapi import FastAPI, HTTPException
from kinde_sdk.management.management_client import ManagementClient
from kinde_sdk.management.exceptions import ApiException
logger = logging.getLogger(__name__)
app = FastAPI()
management = ManagementClient(
domain="your-domain.kinde.com",
client_id="your-management-client-id",
client_secret="your-management-client-secret"
)
@app.get("/users/{user_id}")
def get_user(user_id: str):
try:
return management.users_api.get_user_data(id=user_id)
except ApiException as e:
logger.error("Kinde API error: %s %s", e.status, e.body)
raise HTTPException(status_code=e.status, detail="Failed to fetch user")
except Exception as e:
logger.exception("Unexpected error fetching user %s", user_id)
raise HTTPException(status_code=500, detail="Internal server error")The ManagementClient has its own token management system that is separate from the OAuth authentication clients. It uses the OAuth2 client credentials flow and automatically handles:
- Token acquisition: Obtains tokens using a client credentials grant on the first API call
- Token caching: Tokens are cached and reused to avoid unnecessary requests
- Automatic re-acquisition: Requests new tokens via a client credentials grant when existing ones expire; token lifetime is configurable per application in the Kinde portal
- Shared tokens: Multiple
ManagementClientinstances with the same domain and client ID share the same token
You don't need to manually manage tokens -- the client handles this for you. Note that credentials are validated on the first API call, not during client initialization. If your credentials are incorrect, the error will surface when you make your first request.
- Use the API class properties: Always use
management.users_api,management.organizations_api, etc. The top-level convenience methods (e.g.management.get_users()) are deprecated - Handle API errors appropriately: Use try/except blocks for all Management API calls
- Cache results when appropriate: Reduce API calls by caching user data, organizations, and permissions
- Keep your client credentials secure: Use environment variables, never commit secrets to version control
- Implement retry logic: Add retry logic with exponential backoff for transient failures
For more information about the Management API endpoints and capabilities, see the Kinde Management API documentation.
Proper error handling is crucial for building robust applications with the Kinde Python SDK. This section covers common error scenarios and best practices.
The SDK raises specific exception types that you should handle:
from kinde_sdk.exceptions import (
KindeAPIException,
KindeAuthenticationException,
KindeAuthorizationException,
KindeValidationException,
KindeConfigurationException
)Pattern 1: Comprehensive error handling (FastAPI)
from fastapi import FastAPI, HTTPException
from kinde_sdk.auth.oauth import OAuth
from kinde_sdk.exceptions import (
KindeAPIException,
KindeAuthenticationException,
KindeAuthorizationException
)
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
@app.get("/protected")
async def protected_route(request: Request):
try:
# Check authentication
if not await oauth.is_authenticated(request):
raise HTTPException(status_code=401, detail="Not authenticated")
# Get user info
user_info = await oauth.get_user_info(request)
return user_info
except KindeAuthenticationException as e:
# Handle authentication errors
raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}")
except KindeAuthorizationException as e:
# Handle authorization errors
raise HTTPException(status_code=403, detail=f"Authorization failed: {str(e)}")
except KindeAPIException as e:
# Handle API errors
raise HTTPException(status_code=e.status_code, detail=f"API error: {str(e)}")
except Exception as e:
# Handle unexpected errors
raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")Pattern 2: Error handling with AsyncOAuth
from kinde_sdk.auth.async_oauth import AsyncOAuth
from kinde_sdk.exceptions import KindeAPIException
import logging
oauth = AsyncOAuth()
logger = logging.getLogger(__name__)
async def safe_get_user():
try:
user_info = await oauth.get_user_info()
return user_info
except KindeAPIException as e:
logger.error(f"Kinde API error: {e.status_code} - {e.message}")
return None
except Exception as e:
logger.exception(f"Unexpected error: {str(e)}")
return NonePattern 3: Permission checking with error handling
from fastapi import HTTPException
from kinde_sdk.auth.oauth import OAuth
from kinde_sdk.exceptions import KindeAPIException
oauth = OAuth(framework="fastapi", app=app)
async def check_permission_with_error_handling(request: Request, permission: str):
try:
perm_result = await oauth.get_permission(permission, request)
if not perm_result.get("isGranted"):
raise HTTPException(
status_code=403,
detail=f"Permission '{permission}' not granted"
)
return perm_result
except KindeAPIException as e:
raise HTTPException(
status_code=e.status_code,
detail=f"Error checking permission: {str(e)}"
)- Always validate authentication: Check
is_authenticated()before accessing protected resources - Handle token expiration: Implement token refresh logic or redirect to login
- Validate permissions: Check permissions before allowing operations
- Use appropriate HTTP status codes: 401 for authentication, 403 for authorization failures
- Use environment variables: Never hardcode credentials
- Validate configuration: Check required environment variables at startup
- Use secure storage: Store tokens securely (encrypted sessions, secure cookies)
- Set appropriate session timeouts: Balance security and user experience
- Cache when appropriate: Cache user info, permissions, and feature flags
- Use async operations: Prefer async/await for better performance
- Implement connection pooling: For high-traffic applications
- Batch operations: When possible, batch Management API calls
- Protect client secrets: Use environment variables or secret management services
- Use HTTPS: Always use HTTPS in production
- Validate redirect URLs: Ensure callback URLs are whitelisted
- Implement CSRF protection: Use state parameters in OAuth flows
- Log security events: Log authentication failures and authorization denials
- Handle specific exceptions: Catch specific exception types, not just
Exception - Log errors appropriately: Log errors with context but don't expose sensitive information
- Provide user-friendly messages: Don't expose internal error details to users
- Implement retry logic: For transient failures, implement exponential backoff
- Monitor error rates: Track error rates and patterns in production
- Use type hints: Improve code clarity and IDE support
- Write tests: Test authentication flows, error cases, and edge cases
- Use linting: Catch errors early with tools like
mypyandruff - Document error handling: Document how your application handles errors
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from kinde_sdk.auth.oauth import OAuth
from kinde_sdk.exceptions import (
KindeAPIException,
KindeAuthenticationException,
KindeAuthorizationException,
KindeValidationException
)
import logging
app = FastAPI()
oauth = OAuth(framework="fastapi", app=app)
logger = logging.getLogger(__name__)
# Global exception handler
@app.exception_handler(KindeAuthenticationException)
async def auth_exception_handler(request: Request, exc: KindeAuthenticationException):
logger.warning(f"Authentication failed: {str(exc)}")
return JSONResponse(
status_code=401,
content={"error": "Authentication required", "detail": "Please log in"}
)
@app.exception_handler(KindeAuthorizationException)
async def authz_exception_handler(request: Request, exc: KindeAuthorizationException):
logger.warning(f"Authorization failed: {str(exc)}")
return JSONResponse(
status_code=403,
content={"error": "Access denied", "detail": "Insufficient permissions"}
)
@app.exception_handler(KindeAPIException)
async def api_exception_handler(request: Request, exc: KindeAPIException):
logger.error(f"Kinde API error {exc.status_code}: {str(exc)}")
return JSONResponse(
status_code=exc.status_code,
content={"error": "API error", "detail": "An error occurred with the authentication service"}
)
@app.get("/api/protected")
async def protected_endpoint(request: Request):
# This will automatically handle exceptions via the handlers above
if not await oauth.is_authenticated(request):
raise KindeAuthenticationException("Not authenticated")
permission = await oauth.get_permission("read:data", request)
if not permission.get("isGranted"):
raise KindeAuthorizationException("Permission denied")
return {"message": "Access granted"}