%= form_with(model: [:admin, application], class: "space-y-6", data: { controller: "application-form form-errors" }) do |form| %>
<%= render "shared/form_errors", form: form %>
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "My Application" %>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :slug, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "my-app" %>
Lowercase letters, numbers, and hyphens only. Used in URLs and API calls.
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this application" %>
<% if application.icon.attached? && application.persisted? %>
<% begin %>
<%# Only show icon if we can successfully get its URL (blob is persisted) %>
<% if application.icon.blob&.persisted? && application.icon.blob.key.present? %>
<%= image_tag application.icon, class: "h-16 w-16 rounded-lg object-cover border border-gray-200", alt: "Current icon" %>
Current icon
<%= number_to_human_size(application.icon.blob.byte_size) %>
<% end %>
<% rescue ArgumentError => e %>
<%# Handle case where icon attachment exists but can't generate signed_id %>
<% if e.message.include?("Cannot get a signed_id for a new record") %>
Icon uploaded
File will be processed shortly
<% else %>
<%# Re-raise if it's a different error %>
<% raise e %>
<% end %>
<% end %>
<% end %>
Upload a file
<%= form.file_field :icon,
accept: "image/png,image/jpg,image/jpeg,image/gif,image/svg+xml",
class: "sr-only",
data: {
file_drop_target: "input",
image_paste_target: "input",
action: "change->file-drop#handleFiles"
} %>
or drag and drop
PNG, JPG, GIF, or SVG up to 2MB
💡 Tip: Click here and press Ctrl+V (or Cmd+V) to paste an image from your clipboard
<%= form.label :landing_url, "Landing URL", class: "block text-sm font-medium text-gray-700" %>
<%= form.url_field :landing_url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "https://app.example.com" %>
The main URL users will visit to access this application. This will be shown as a link on their dashboard.
<%= form.label :app_type, "Application Type", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :app_type, [["OpenID Connect (OIDC)", "oidc"], ["Forward Auth (Reverse Proxy)", "forward_auth"]], {}, {
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
disabled: application.persisted?,
data: { action: "change->application-form#updateFieldVisibility", application_form_target: "appTypeSelect" }
} %>
<% if application.persisted? %>
Application type cannot be changed after creation.
<% end %>
OIDC Configuration
<% unless application.persisted? %>
Client Type
<%= form.radio_button :is_public_client, "false", checked: !application.is_public_client, class: "mt-1 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500", data: { action: "change->application-form#updatePkceVisibility" } %>
Confidential Client (Recommended)
Backend server app that can securely store a client secret. Examples: traditional web apps, server-to-server APIs.
<%= form.radio_button :is_public_client, "true", checked: application.is_public_client, class: "mt-1 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500", data: { action: "change->application-form#updatePkceVisibility" } %>
Public Client
Frontend-only app that cannot store secrets securely. Examples: SPAs (React/Vue), mobile apps, CLI tools. PKCE is required.
<% else %>
Client Type:
<% if application.public_client? %>
Public Client (PKCE Required)
<% else %>
Confidential Client
<% end %>
<% end %>
OAuth2 Flow
Clinch uses the authorization_code flow with response_type=code (the modern, secure standard).
Deprecated flows like Implicit (id_token, token) are not supported for security reasons.
Client Authentication
Clinch supports both client_secret_basic (HTTP Basic Auth) and client_secret_post (POST parameters) authentication methods.
<%= form.check_box :require_pkce, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
<%= form.label :require_pkce, "Require PKCE (Proof Key for Code Exchange)", class: "ml-2 block text-sm font-medium text-gray-900" %>
Recommended for enhanced security (OAuth 2.1 best practice).
Note: Public clients always require PKCE regardless of this setting.
<%= form.check_box :skip_consent, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
<%= form.label :skip_consent, "Skip Consent Screen", class: "ml-2 block text-sm font-medium text-gray-900" %>
Automatically grant consent for all users. Useful for first-party or trusted applications.
Only enable for applications you fully trust. Consent is still recorded in the database.
<%= form.label :redirect_uris, "Redirect URIs", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :redirect_uris, rows: 4, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "https://example.com/callback\nhttps://app.example.com/auth/callback" %>
One URI per line. These are the allowed callback URLs for your application.
<%= form.label :backchannel_logout_uri, "Backchannel Logout URI (Optional)", class: "block text-sm font-medium text-gray-700" %>
<%= form.url_field :backchannel_logout_uri, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "https://app.example.com/oidc/backchannel-logout" %>
If the application supports OpenID Connect Backchannel Logout, enter the logout endpoint URL.
When users log out, Clinch will send logout notifications to this endpoint for immediate session termination.
Leave blank if the application doesn't support backchannel logout.
Token Expiration Settings
Configure how long tokens remain valid. Shorter times are more secure but require more frequent refreshes.
<%= form.label :access_token_ttl, "Access Token TTL", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :access_token_ttl,
value: application.access_token_ttl || "1h",
placeholder: "e.g., 1h, 30m, 3600",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono" %>
Range: 5m - 24h
Default: 1h
<% if application.access_token_ttl.present? %>
Current: <%= application.access_token_ttl_human %> (<%= application.access_token_ttl %>s)
<% end %>
<%= form.label :refresh_token_ttl, "Refresh Token TTL", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :refresh_token_ttl,
value: application.refresh_token_ttl || "30d",
placeholder: "e.g., 30d, 1M, 2592000",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono" %>
Range: 5m - 90d
Default: 30d
<% if application.refresh_token_ttl.present? %>
Current: <%= application.refresh_token_ttl_human %> (<%= application.refresh_token_ttl %>s)
<% end %>
<%= form.label :id_token_ttl, "ID Token TTL", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :id_token_ttl,
value: application.id_token_ttl || "1h",
placeholder: "e.g., 1h, 30m, 3600",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono" %>
Range: 5m - 24h
Default: 1h
<% if application.id_token_ttl.present? %>
Current: <%= application.id_token_ttl_human %> (<%= application.id_token_ttl %>s)
<% end %>
Understanding Token Types & Session Length
Token Types:
Access Token: Used to access protected resources (APIs). Shorter lifetime = more secure. Users won't notice automatic refreshes.
Refresh Token: Used to get new access tokens without re-authentication. Each refresh issues a new refresh token (token rotation).
ID Token: Contains user identity information (JWT). Should match access token lifetime in most cases.
How Session Length Works:
Refresh Token TTL = Maximum Inactivity Period
Because refresh tokens are automatically rotated (new token = new expiry), active users can stay logged in indefinitely. The TTL controls how long they can be inactive before requiring re-authentication.
Example: Refresh TTL = 30 days
User logs in on Day 0, uses app daily → stays logged in forever (tokens keep rotating)
User logs in on Day 0, stops using app → must re-login after 30 days of inactivity
Forcing Re-Authentication:
Because of token rotation, there's no way to force periodic re-authentication using TTL settings alone. Active users can stay logged in indefinitely by refreshing tokens before they expire.
To enforce absolute session limits: Clients can include the max_age parameter in their authorization requests to require re-authentication after a specific time, regardless of token rotation.
Example: A banking app might set max_age=900 (15 minutes) in the authorization request to force re-authentication every 15 minutes, even if refresh tokens are still valid.
Common Configurations:
Banking/High Security: Access TTL = 5m, Refresh TTL = 5m → Re-auth every 5 minutes
Corporate Tools: Access TTL = 1h, Refresh TTL = 8h → Re-auth after 8 hours inactive
Personal Apps: Access TTL = 1h, Refresh TTL = 30d → Re-auth after 30 days inactive
Forward Auth Configuration
<%= form.label :domain_pattern, "Domain Pattern", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :domain_pattern, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "*.example.com or app.example.com" %>
Domain pattern to match. Use * for wildcard subdomains (e.g., *.example.com matches app.example.com, api.example.com, etc.)
<%= form.label :headers_config, "Custom Headers Configuration (JSON)", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :headers_config, value: (application.headers_config.present? && application.headers_config.any? ? JSON.pretty_generate(application.headers_config) : ""), rows: 10,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
placeholder: '{"user": "Remote-User", "groups": "Remote-Groups"}',
data: {
action: "input->json-validator#validate blur->json-validator#format",
json_validator_target: "textarea"
} %>
Optional: Customize header names sent to your application.
Format JSON
Insert Example
Default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Username, X-Remote-Groups, X-Remote-Admin
Show available header keys and what data they send
user - User's email address
email - User's email address
name - User's display name (falls back to email if not set)
username - User's login username (only sent if set)
groups - Comma-separated list of group names (e.g., "admin,developers")
admin - "true" or "false" indicating admin status
Example: {"user": "Remote-User", "groups": "Remote-Groups", "username": "Remote-Username"}
Need custom user fields? Add them to user's custom_claims for OIDC tokens
<%= form.label :group_ids, "Allowed Groups (Optional)", class: "block text-sm font-medium text-gray-700" %>
<% if @available_groups.any? %>
<% @available_groups.each do |group| %>
<%= check_box_tag "application[group_ids][]", group.id, application.allowed_groups.include?(group), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
<%= label_tag "application_group_ids_#{group.id}", group.name, class: "ml-2 text-sm text-gray-900" %>
(<%= pluralize(group.users.count, "member") %>)
<% end %>
<% else %>
No groups available. Create groups first to restrict access.
<% end %>
If no groups are selected, all active users can access this application.
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
<%= form.submit application.persisted? ? "Update Application" : "Create Application", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
<%= link_to "Cancel", admin_applications_path, class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
<% end %>