Audience: Coding/research agents building Frappe apps without direct repo access.
Goal: Provide implementation-ready understanding of OAuth, credential storage, Connected App, and Google integration patterns in Frappe.
Frappe has two parallel auth/integration systems you need to distinguish clearly:
-
Frappe as OAuth Provider / OpenID Provider (server-side provider endpoints)
- Backed by
frappe/integrations/oauth2.py+frappe/oauth.py+ OAuth DocTypes. - Supports authorization flows for client apps authenticating against Frappe.
- Backed by
-
Frappe as OAuth Client (outbound integration to external providers)
- Modern/generalized path:
Connected App+Token Cache. - Legacy/specialized paths:
Social Login Key+ helpers infrappe/utils/oauth.py, and Google-specific modules.
- Modern/generalized path:
If you’re building new external integrations (Slack/ClickUp/etc), default to Connected App + Token Cache unless forced otherwise.
oauthlibOpenID server (WebApplicationServer) for provider endpoints.OAuthWebRequestValidator(Frappe custom validator) bridges OAuthlib ↔ DocTypes.pydanticfor dynamic client registration metadata validation.
requests_oauthlib.OAuth2Sessionfor web app and refresh token flows.oauthlib.oauth2.BackendApplicationClientfor machine-to-machine token fetch.
passlib(pbkdf2_sha256,argon2) for login password hashing.cryptography.fernetfor reversible encryption of API secrets/token-like password fields.
google.oauth2.credentialsandgoogleapiclient.discovery.buildfor Google APIs.- Raw
requests.post/getfor token exchange and token validation.
These are method endpoints under /api/method/...:
| Endpoint | Method | Auth | Handler | Purpose |
|---|---|---|---|---|
/api/method/frappe.integrations.oauth2.authorize |
GET | Guest allowed, login required for approval | authorize(**kwargs) |
Validate auth request, optionally render consent or redirect directly |
/api/method/frappe.integrations.oauth2.approve |
GET | Logged-in | approve(*args, **kwargs) |
Finalize consent and issue authorization response |
/api/method/frappe.integrations.oauth2.get_token |
POST/form | Guest allowed | get_token(*args, **kwargs) |
Token endpoint (authorization code, refresh, password grant) |
/api/method/frappe.integrations.oauth2.revoke_token |
POST/form | Guest allowed | revoke_token(*args, **kwargs) |
Revoke access or refresh token |
/api/method/frappe.integrations.oauth2.openid_profile |
GET/POST | Auth required | openid_profile(*args, **kwargs) |
UserInfo endpoint |
/api/method/frappe.integrations.oauth2.introspect_token |
GET/POST | Guest allowed | introspect_token(token, token_type_hint) |
Token metadata/introspection |
/api/method/frappe.integrations.oauth2.register_client |
POST JSON | Guest allowed, feature-flagged | register_client() |
Dynamic OAuth client registration |
Handled by handle_wellknown(path):
/.well-known/openid-configuration/.well-known/oauth-authorization-server(if enabled)/.well-known/oauth-protected-resource(if enabled)
Supported. Primary secure flow.
- Auth request validated in
authorize()andOAuthWebRequestValidator.validate_*methods. - Code persisted in
OAuth Authorization Codebysave_authorization_code(). - Exchanged at token endpoint (
get_token()→ OAuthlib →save_bearer_token()).
Supported.
code_challengeandcode_challenge_methodstored in authorization code doc.validate_code()checkscode_verifier(plainands256).
Supported.
validate_refresh_token()validates active refresh token.- Token refresh creates new bearer record via
save_bearer_token().
Technically supported via validate_grant_type() including password, and validate_user() using LoginManager.authenticate.
- Treat as legacy/high-risk; avoid for new third-party integrations.
OAuth ClientDocType still hasgrant_type=Implicit/response_type=Token.- Test coverage includes implicit token redirect behavior.
- But metadata endpoint intentionally only advertises
response_types_supported=["code"]for secure posture.
- Provider side: not broadly exposed as first-class advertised grant in metadata.
- Connected App side (outbound client): supported via
get_backend_app_token()usingBackendApplicationClient.
OAuthWebRequestValidator is the core extension point:
-
Client/redirect/scope checks:
validate_client_id(client_id, request, ...)validate_redirect_uri(client_id, redirect_uri, request, ...)validate_scopes(client_id, scopes, client, request, ...)validate_response_type(client_id, response_type, client, request, ...)
-
Code/token persistence:
save_authorization_code(client_id, code, request, ...)save_bearer_token(token, request, ...)invalidate_authorization_code(client_id, code, request, ...)
-
Token operations:
validate_bearer_token(token, scopes, request)validate_refresh_token(refresh_token, client, request, ...)revoke_token(token, token_type_hint, request, ...)
-
OIDC claims/token:
finalize_id_token(...)get_jwt_bearer_token(...)get_userinfo_claims(request)
-
Password grant auth:
validate_user(username, password, client, request, ...)
If you customize provider behavior, this class is your highest-leverage hook.
Frappe stores sensitive values in __Auth with two modes:
-
Hashed (
encrypted = 0)- For login passwords.
- Verified by
passlibctx.verify(). - Written by
update_password().
-
Encrypted (
encrypted = 1)- For reversible secrets (API keys, OAuth client secrets, refresh tokens in Password fields).
- Written by
set_encrypted_password(). - Read via
get_decrypted_password(). - Cipher: Fernet key from
site_config.encryption_key.
- Use
Passwordfields for secrets you need to read back (tokens/client secrets). - Never store OAuth secrets in plain
Datafields. - Encryption key lifecycle is operationally critical (restores/migrations must preserve key).
Purpose: Registers third-party client apps that authenticate against Frappe.
Key fields:
client_id(set to document name)client_secretredirect_uris,default_redirect_urigrant_type(Authorization Code/Implicit)response_type(Code/Token)scopestoken_endpoint_auth_method(Client Secret Basic/Client Secret Post/None)allowed_roles(role gate)
Important methods:
validate()(assign client_id, default secret, enforce grant/response compatibility)user_has_allowed_role()(session role authorization)is_public_client()
Purpose: Stores provider-side issued access/refresh token records.
Key fields:
access_token(autoname)refresh_tokenclient,user,scopesexpires_in,expiration_time,status
Method:
clear_old_logs(days=30)cleanup helper.
Purpose: Temporary authorization code storage.
Key fields:
authorization_code(autoname)client,user,scopesredirect_uri_bound_to_authorization_codenonce,code_challenge,code_challenge_methodvalidity
Purpose: feature toggles and metadata publishing.
Key fields include:
show_auth_server_metadatashow_protected_resource_metadataenable_dynamic_client_registrationskip_authorizationallowed_public_client_origins- resource metadata descriptors (
resource_name, docs/policy/tos)
skip_authorization(Force/Auto).- Used by compatibility function
get_oauth_settings().
Purpose: Generic outbound OAuth 2.0 client config.
Key fields:
- Provider identity:
provider_name - Discovery:
openid_configuration - Client creds:
client_id,client_secret, computedredirect_uri - Endpoints:
authorization_uri,token_uri,userinfo_uri,introspection_uri,revocation_uri - Scopes table
- Query parameter table (for provider-specific extras)
Key methods:
get_openid_configuration()get_oauth2_session(user=None, init=False)initiate_web_application_flow(user=None, success_uri=None)get_user_token(...)get_active_token(user=None)(refresh handling)get_backend_app_token(include_client_id=None)(machine-to-machine)
Purpose: Outbound token persistence per (connected_app, user).
Naming pattern:
autoname = format:{connected_app}-{user}
Key fields:
access_token(Password)refresh_token(Password)expires_in,token_typestate,success_uriscopeschild table
Key methods:
update_data(data)is_expired()get_json()get_auth_header()
- Admin creates
Connected App. validate()auto-sets callback URL to:/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/<app_name>
- User clicks Connect to {provider} in form UI.
initiate_web_application_flow():- builds OAuth2 session (init mode)
- calls
authorization_url(...)on provider auth endpoint - stores
state+ optionalsuccess_uriinToken Cache
- Browser redirected to provider authorization UI.
- Provider returns to callback endpoint with
code+state. callback()validates state, exchanges code for token viafetch_token(...).token_cache.update_data(token)stores encrypted token fields.- User redirected to
success_urior app form URL. - Business code obtains reusable session via
get_oauth2_session(user); token refresh can happen automatically.
frappe.integrations.oauth2.authorize(**kwargs)frappe.integrations.oauth2.approve(*args, **kwargs)frappe.integrations.oauth2.get_token(*args, **kwargs)frappe.integrations.oauth2.revoke_token(*args, **kwargs)frappe.integrations.oauth2.openid_profile(*args, **kwargs)frappe.integrations.oauth2.introspect_token(token, token_type_hint=None)frappe.integrations.oauth2.register_client()(POST JSON)
ConnectedApp.get_openid_configuration(self)ConnectedApp.initiate_web_application_flow(self, user=None, success_uri=None)frappe.integrations.doctype.connected_app.connected_app.callback(code=None, state=None)frappe.integrations.doctype.connected_app.connected_app.has_token(connected_app, connected_user=None)
frappe.integrations.doctype.google_settings.google_settings.get_file_picker_settings()frappe.integrations.doctype.google_calendar.google_calendar.authorize_access(g_calendar, reauthorize=False)frappe.integrations.doctype.google_calendar.google_calendar.google_callback(code=None)frappe.integrations.doctype.google_calendar.google_calendar.sync(g_calendar=None)frappe.integrations.google_oauth.callback(state, code=None, error=None)
frappe.auth.get_logged_user()
Connected App+Token Cachefor external OAuth provider integration.OAuth2Sessionwithauto_refresh_url+token_updater.Passwordfieldtype storage for reversible secrets.- OIDC discovery via
get_openid_configuration()when provider supports it.
- Add tenant/workspace identifiers to token caching strategy (if one user connects multiple accounts per provider).
- Add robust refresh failure retries/backoff and observability logs.
- Add explicit token revocation call flow when disconnecting apps.
- Hardcoding provider URLs/scopes directly in app logic (Google Calendar legacy style).
- Storing long-lived tokens outside
Password/encrypted pattern. - Building net-new bespoke OAuth flow before checking Connected App fit.
Google modules (Google Settings, Google Calendar, google_oauth.py) predate/parallel Connected App and include provider-specific coupling:
- Hardcoded Google URLs/scopes.
- Custom callback routing with domain dispatch map.
- Separate token fields in Google-specific DocTypes.
- Mixed direct
requestsand Google SDK use.
For new non-Google integrations, do not copy this pattern. Treat it as legacy compatibility layer.
- Create Connected App record with endpoints, client credentials, scopes.
- Trigger
initiate_web_application_flow()from UI action. - On success, use
get_active_token()orget_oauth2_session()in server logic. - Use
token_cache.get_auth_header()or OAuth2 session request methods.
app = frappe.get_doc("Connected App", "My Provider")
session = app.get_oauth2_session(user=frappe.session.user)
response = session.get("https://api.example.com/v1/me")
response.raise_for_status()
data = response.json()app = frappe.get_doc("Connected App", "Service Principal App")
token_cache = app.get_backend_app_token(include_client_id=True)
headers = token_cache.get_auth_header()
result = frappe.integrations.utils.make_get_request("https://api.example.com/system", headers=headers)- For reconnect: rerun
initiate_web_application_flow(). - For disconnect: clear/rotate token cache + call provider revocation endpoint if available.
- Provider metadata endpoint advertises only code flow despite historical implicit support.
- Password grant exists but should be treated as legacy.
Connected Apptoken cache key is app+user; multi-account-per-user needs custom extension.- State and success URI are stored in Token Cache, so callback depends on pre-created state record.
- Encryption key mismatch breaks decryption of stored Password-field secrets.
| Service pattern | Recommended path in Frappe |
|---|---|
| Standard OAuth2 auth code + refresh | Connected App directly |
| OAuth2 auth code + PKCE | Connected App (if provider supports your selected params) + verify query params/code verifier handling |
| Client credentials only | Connected App get_backend_app_token() |
| API key / static bearer only | Custom DocType with Password fields + integration utils request wrappers |
| Non-OAuth auth (e.g., XML-RPC creds) | Custom auth adapter; do not force into Connected App |
# frappe/integrations/oauth2.py
approve(*args, **kwargs)
authorize(**kwargs)
get_token(*args, **kwargs)
revoke_token(*args, **kwargs)
openid_profile(*args, **kwargs)
introspect_token(token: str, token_type_hint: str | None = None)
register_client() # POST JSON
# frappe/integrations/doctype/connected_app/connected_app.py
ConnectedApp.get_openid_configuration(self)
ConnectedApp.get_oauth2_session(self, user=None, init=False)
ConnectedApp.initiate_web_application_flow(self, user: str | None = None, success_uri: str | None = None)
ConnectedApp.get_user_token(self, user=None, success_uri=None)
ConnectedApp.get_active_token(self, user=None)
ConnectedApp.get_backend_app_token(self, include_client_id=None)
callback(code: str | None = None, state: str | None = None)
has_token(connected_app: str, connected_user: str | None = None)
# frappe/integrations/doctype/token_cache/token_cache.py
TokenCache.get_auth_header(self)
TokenCache.update_data(self, data)
TokenCache.get_expires_in(self)
TokenCache.is_expired(self)
TokenCache.get_json(self)
# frappe/utils/password.py
get_decrypted_password(doctype, name, fieldname="password", raise_exception=True)
set_encrypted_password(doctype, name, pwd, fieldname="password")
update_password(user, pwd, doctype="User", fieldname="password", logout_all_sessions=False)
check_password(user, pwd, doctype="User", fieldname="password", delete_tracker_cache=True)Use
Connected AppandToken Cacheonly. Do not use Google-specific modules. Add a server method that callsinitiate_web_application_flow, validate callback state, persist token inToken Cache, and expose a helper that returns an auto-refreshingOAuth2Session.
Secrets must live in
Passwordfields /__Authencrypted mode via Frappe password utils. Never store tokens or client secrets in plainDatafields.
Log outbound API failures with integration request logs and include provider/app/user identifiers (without printing secrets).
If you only remember five rules:
- Default to Connected App + Token Cache for outbound OAuth.
- Treat Google modules as legacy/specialized, not architecture templates.
- Use
Passwordfields for all reversible credentials. - Prefer auth code + refresh (+ PKCE where required).
- Keep token lifecycle explicit: issue, cache, refresh, revoke, rotate.