Skip to content

OAuth Provider

github-actions[bot] edited this page May 20, 2026 · 3 revisions

OAuth Provider

Daptin can act as an OAuth 2.0 and OpenID Connect provider. Client applications can send users to Daptin, receive an authorization code, exchange it for access and refresh tokens, and call Daptin's userinfo/introspection endpoints.

This page covers Daptin issuing tokens. For Daptin consuming tokens from Google, GitHub, Dropbox, or another upstream provider, see OAuth-Authentication and Daptin-Google-OAuth-Complete-Flow.

When another Daptin instance or app uses this provider for browser login, that app is the OAuth client. Register the redirect URI for the client origin that the user's browser will actually visit. If an app frontend proxies to a Daptin OAuth client backend, register the frontend callback URL, such as https://app.example.com/oauth/response?authenticator=daptin-login, not an unrelated admin dashboard or backend-only host.

Current Scope

Capability Supported
OAuth authorization code flow Yes
Refresh token flow Yes
PKCE S256 Yes, required
PKCE plain Yes, compatibility only
OpenID Connect discovery Yes
RS256 ID tokens Yes
JWKS Yes
UserInfo Yes
Token introspection Yes
Token revocation Yes
Implicit flow No
Client credentials flow No

Provider Tables

The provider uses compact system table names.

Table Purpose
oauth_app Registered OAuth clients
oauth_code Authorization codes, stored as hashes
oauth_access Access tokens, stored as hashes
oauth_refresh Refresh tokens, stored as hashes
oauth_grant Reserved for explicit user grants and consent records
oauth_key RS256 signing keys for ID tokens and JWKS

Do not confuse these with the existing OAuth consumer tables:

Table Purpose
oauth_connect External provider connection config
oauth_token Tokens received from external providers

Endpoints

Endpoint Method Purpose
/.well-known/oauth-authorization-server GET OAuth metadata
/.well-known/openid-configuration GET OIDC discovery
/oauth/authorize GET Create an authorization code for a signed-in Daptin user
/oauth/token POST Exchange authorization codes and refresh tokens
/oauth/revoke POST Revoke access or refresh token
/oauth/introspect POST Check whether an access token is active
/oauth/userinfo GET, POST Return claims for the access token user
/oauth/jwks GET Public signing keys

Register a Client

Register clients through the Daptin action API. oauth_app is the internal storage table and is not the public management API.

curl -X POST http://localhost:6336/action/oauth_app/register_client \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": {
      "name": "Example Client",
      "redirect_uris": "https://client.example.com/callback",
      "scopes": "openid profile email",
      "grants": "authorization_code,refresh_token",
      "is_confidential": true
    }
  }'

The action returns generated client_id and client_secret. Store the secret immediately; it is returned only on registration and rotation.

Client Management Actions

Endpoint Purpose
POST /action/oauth_app/register_client Register a new client
POST /action/oauth_app/update_client Update client metadata
POST /action/oauth_app/rotate_client_secret Generate and return a new secret
POST /action/oauth_app/disable_client Disable a client
POST /action/oauth_app/enable_client Enable a client
POST /action/oauth_app/revoke_client_tokens Revoke access and refresh tokens for the client

Instance-bound actions pass the app reference ID:

{
  "attributes": {
    "oauth_app_id": "OAUTH_APP_REFERENCE_ID"
  }
}
Field Notes
client_id Public OAuth identifier generated by Daptin
client_secret Required for confidential clients; generated by Daptin and returned only once
redirect_uris Whitespace or comma separated exact redirect URI allow-list
scopes Whitespace or comma separated allowed scopes
grants Usually authorization_code,refresh_token
is_confidential If true, /oauth/token, /oauth/revoke, and /oauth/introspect require client secret auth
is_enabled Disable a client without deleting it

Authorization Code Flow

1. Create a PKCE challenge

The client creates a random code_verifier, then sends:

code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = S256

PKCE is required. Requests without code_challenge fail before an authorization code is issued.

2. Redirect the user to Daptin

GET /oauth/authorize?
  response_type=code&
  client_id=example-client&
  redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
  scope=openid%20profile%20email&
  state=random-csrf-state&
  code_challenge=BASE64URL_SHA256_VERIFIER&
  code_challenge_method=S256&
  nonce=random-oidc-nonce

The user must be signed in to Daptin. If not, Daptin redirects to oauth.login_url from backend config, or /auth/signin by default. The built-in /auth/signin page accepts email/password, sets the normal Daptin token cookie, and returns to the original /oauth/authorize request through the same-origin return_to parameter.

Daptin validates:

  • response_type=code
  • client exists and is enabled
  • exact redirect_uri match
  • requested grant is allowed
  • requested scopes are allowed
  • PKCE challenge is present
  • signed-in Daptin user exists

Invalid redirect URIs return HTTP 400 directly and are not redirected.

The redirect_uri must exactly match one of the registered client redirect URIs. For Daptin OAuth consumer callbacks, include the authenticator query parameter in the provider registration because the Daptin consumer appends it when creating the authorization URL.

3. Exchange the code

curl -X POST http://localhost:6336/oauth/token \
  -u "example-client:replace-with-a-long-random-secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=https://client.example.com/callback" \
  -d "code_verifier=$CODE_VERIFIER"

Response:

{
  "access_token": "opaque-access-token",
  "refresh_token": "opaque-refresh-token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email",
  "id_token": "eyJ..."
}

The authorization code is single-use. Replaying it returns invalid_grant.

Built-in Login Page

Daptin includes a minimal provider login page for browser OAuth flows:

Endpoint Method Purpose
/auth/signin GET Render the Daptin login form
/auth/signin POST Validate email/password, set the Daptin session cookie, and redirect to return_to

return_to must be a same-origin absolute path such as /oauth/authorize?.... Absolute external URLs and protocol-relative URLs are rejected and replaced with /.

This page is separate from the admin SPA /sign-in route. OAuth provider redirects use /auth/signin so Daptin can complete the server-side login and continue the authorization request without depending on frontend application routing.

4. Refresh

curl -X POST http://localhost:6336/oauth/token \
  -u "example-client:replace-with-a-long-random-secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN"

Refresh tokens rotate on use. The old refresh token is revoked and replay fails.

UserInfo

curl http://localhost:6336/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Typical response:

{
  "sub": "user-reference-id",
  "email": "user@example.com",
  "name": "User Name"
}

Introspection

curl -X POST http://localhost:6336/oauth/introspect \
  -u "example-client:replace-with-a-long-random-secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN"

Active response:

{
  "active": true,
  "client_id": "example-client",
  "scope": "openid profile email",
  "token_type": "Bearer",
  "exp": 1777445590,
  "sub": "user-reference-id"
}

Revocation

curl -X POST http://localhost:6336/oauth/revoke \
  -u "example-client:replace-with-a-long-random-secret" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN"

OpenID Connect

Discovery:

GET /.well-known/openid-configuration

JWKS:

GET /oauth/jwks

ID tokens use RS256 and include the active oauth_key key ID in the JWT header.

Backend Config

Key Default Purpose
oauth.issuer request scheme and host Issuer used in metadata and ID tokens
oauth.login_url /auth/signin Login redirect when /oauth/authorize has no Daptin user session
encryption.secret instance default Encrypts the OAuth signing private key

Set an issuer:

curl -X POST http://localhost:6336/_config/backend/oauth.issuer \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d "https://auth.example.com"

Security Notes

  • Use HTTPS in production.
  • Use exact redirect URIs. Wildcards are not supported.
  • Use S256 PKCE for all clients.
  • Authorization codes expire after 10 minutes and are single-use.
  • Access tokens expire after 1 hour.
  • Refresh tokens expire after 30 days and rotate on refresh.
  • Bearer tokens and authorization codes are stored as SHA-256 hashes.
  • Signing private keys are encrypted at rest in oauth_key.
  • Token responses set Cache-Control: no-store and Pragma: no-cache.

Current Limits

  • There is no separate consent screen yet. A signed-in Daptin user authorizes the app directly when /oauth/authorize is called.
  • oauth_grant exists for future explicit grant/consent records, but the current authorization handler does not depend on it.
  • OAuth client management is exposed through Daptin actions. Dynamic standards-based client registration is not implemented.

Clone this wiki locally