Merge branch 'main' into feature/claims
This commit is contained in:
@@ -75,6 +75,9 @@ Apps that speak OIDC use the OIDC flow.
|
|||||||
Apps that only need "who is it?", or you want available from the internet behind authentication (MeTube, Jellyfin) use ForwardAuth.
|
Apps that only need "who is it?", or you want available from the internet behind authentication (MeTube, Jellyfin) use ForwardAuth.
|
||||||
|
|
||||||
#### OpenID Connect (OIDC)
|
#### OpenID Connect (OIDC)
|
||||||
|
|
||||||
|
**[OpenID Certified](https://www.certification.openid.net/plan-detail.html?plan=FbQNTJuYVzrzs&public=true)** - Clinch passes the official OpenID Connect conformance tests (valid as of [v0.8.6](https://github.com/dkam/clinch/releases/tag/0.8.6)).
|
||||||
|
|
||||||
Standard OAuth2/OIDC provider with endpoints:
|
Standard OAuth2/OIDC provider with endpoints:
|
||||||
- `/.well-known/openid-configuration` - Discovery endpoint
|
- `/.well-known/openid-configuration` - Discovery endpoint
|
||||||
- `/authorize` - Authorization endpoint with PKCE support
|
- `/authorize` - Authorization endpoint with PKCE support
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ActiveSessionsController < ApplicationController
|
|||||||
Rails.logger.info "ActiveSessionsController: Logged out from #{application.name} - revoked #{revoked_access_tokens} access tokens and #{revoked_refresh_tokens} refresh tokens"
|
Rails.logger.info "ActiveSessionsController: Logged out from #{application.name} - revoked #{revoked_access_tokens} access tokens and #{revoked_refresh_tokens} refresh tokens"
|
||||||
|
|
||||||
# Keep the consent intact - this is the key difference from revoke_consent
|
# Keep the consent intact - this is the key difference from revoke_consent
|
||||||
redirect_to root_path, notice: "Successfully logged out of #{application.name}."
|
redirect_to root_path, notice: "Revoked access tokens for #{application.name}. Re-authentication will be required on next use."
|
||||||
end
|
end
|
||||||
|
|
||||||
def revoke_all_consents
|
def revoke_all_consents
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ module Admin
|
|||||||
permitted = params.require(:application).permit(
|
permitted = params.require(:application).permit(
|
||||||
:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata,
|
:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata,
|
||||||
:domain_pattern, :landing_url, :access_token_ttl, :refresh_token_ttl, :id_token_ttl,
|
:domain_pattern, :landing_url, :access_token_ttl, :refresh_token_ttl, :id_token_ttl,
|
||||||
:icon, :backchannel_logout_uri, :is_public_client, :require_pkce
|
:icon, :backchannel_logout_uri, :is_public_client, :require_pkce, :skip_consent
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle headers_config - it comes as a JSON string from the text area
|
# Handle headers_config - it comes as a JSON string from the text area
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ class Application < ApplicationRecord
|
|||||||
# When true, no client_secret will be generated (public client)
|
# When true, no client_secret will be generated (public client)
|
||||||
attr_accessor :is_public_client
|
attr_accessor :is_public_client
|
||||||
|
|
||||||
|
# Virtual setters for TTL fields - accept human-friendly durations
|
||||||
|
# e.g., "1h", "30m", "1d", or plain numbers "3600"
|
||||||
|
def access_token_ttl=(value)
|
||||||
|
parsed = DurationParser.parse(value)
|
||||||
|
super(parsed)
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_token_ttl=(value)
|
||||||
|
parsed = DurationParser.parse(value)
|
||||||
|
super(parsed)
|
||||||
|
end
|
||||||
|
|
||||||
|
def id_token_ttl=(value)
|
||||||
|
parsed = DurationParser.parse(value)
|
||||||
|
super(parsed)
|
||||||
|
end
|
||||||
|
|
||||||
has_one_attached :icon
|
has_one_attached :icon
|
||||||
|
|
||||||
# Fix SVG content type after attachment
|
# Fix SVG content type after attachment
|
||||||
@@ -39,7 +56,7 @@ class Application < ApplicationRecord
|
|||||||
|
|
||||||
# Token TTL validations (for OIDC apps)
|
# Token TTL validations (for OIDC apps)
|
||||||
validates :access_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
validates :access_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
||||||
validates :refresh_token_ttl, numericality: {greater_than_or_equal_to: 86400, less_than_or_equal_to: 7776000}, if: :oidc? # 1 day - 90 days
|
validates :refresh_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 7776000}, if: :oidc? # 5 min - 90 days
|
||||||
validates :id_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
validates :id_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
||||||
|
|
||||||
normalizes :slug, with: ->(slug) { slug.strip.downcase }
|
normalizes :slug, with: ->(slug) { slug.strip.downcase }
|
||||||
|
|||||||
@@ -153,6 +153,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<!-- OAuth2/OIDC Flow Information -->
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900 mb-2">OAuth2 Flow</h4>
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
Clinch uses the <code class="bg-white px-1.5 py-0.5 rounded text-xs font-mono">authorization_code</code> flow with <code class="bg-white px-1.5 py-0.5 rounded text-xs font-mono">response_type=code</code> (the modern, secure standard).
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">
|
||||||
|
Deprecated flows like Implicit (<code class="bg-white px-1 rounded text-xs font-mono">id_token</code>, <code class="bg-white px-1 rounded text-xs font-mono">token</code>) are not supported for security reasons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-blue-200 pt-3">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900 mb-2">Client Authentication</h4>
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
Clinch supports both <code class="bg-white px-1.5 py-0.5 rounded text-xs font-mono">client_secret_basic</code> (HTTP Basic Auth) and <code class="bg-white px-1.5 py-0.5 rounded text-xs font-mono">client_secret_post</code> (POST parameters) authentication methods.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- PKCE Requirement (only for confidential clients) -->
|
<!-- PKCE Requirement (only for confidential clients) -->
|
||||||
<div id="pkce-options" data-application-form-target="pkceOptions" class="<%= 'hidden' if application.persisted? && application.public_client? %>">
|
<div id="pkce-options" data-application-form-target="pkceOptions" class="<%= 'hidden' if application.persisted? && application.public_client? %>">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -165,6 +185,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Skip Consent -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= 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" %>
|
||||||
|
</div>
|
||||||
|
<p class="ml-6 text-sm text-gray-500">
|
||||||
|
Automatically grant consent for all users. Useful for first-party or trusted applications.
|
||||||
|
<br><span class="text-xs text-amber-600">Only enable for applications you fully trust. Consent is still recorded in the database.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :redirect_uris, "Redirect URIs", class: "block text-sm font-medium text-gray-700" %>
|
<%= 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" %>
|
<%= 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" %>
|
||||||
@@ -187,43 +217,90 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :access_token_ttl, "Access Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
<%= form.label :access_token_ttl, "Access Token TTL", class: "block text-sm font-medium text-gray-700" %>
|
||||||
<%= form.number_field :access_token_ttl, value: application.access_token_ttl || 3600, min: 300, max: 86400, step: 60, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
<%= 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" %>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
Range: 5 min - 24 hours
|
Range: 5m - 24h
|
||||||
<br>Default: 1 hour (3600s)
|
<br>Default: 1h
|
||||||
<br>Current: <span class="font-medium"><%= application.access_token_ttl_human || "1 hour" %></span>
|
<% if application.access_token_ttl.present? %>
|
||||||
|
<br>Current: <span class="font-medium"><%= application.access_token_ttl_human %> (<%= application.access_token_ttl %>s)</span>
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :refresh_token_ttl, "Refresh Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
<%= form.label :refresh_token_ttl, "Refresh Token TTL", class: "block text-sm font-medium text-gray-700" %>
|
||||||
<%= form.number_field :refresh_token_ttl, value: application.refresh_token_ttl || 2592000, min: 86400, max: 7776000, step: 86400, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
<%= 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" %>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
Range: 1 day - 90 days
|
Range: 5m - 90d
|
||||||
<br>Default: 30 days (2592000s)
|
<br>Default: 30d
|
||||||
<br>Current: <span class="font-medium"><%= application.refresh_token_ttl_human || "30 days" %></span>
|
<% if application.refresh_token_ttl.present? %>
|
||||||
|
<br>Current: <span class="font-medium"><%= application.refresh_token_ttl_human %> (<%= application.refresh_token_ttl %>s)</span>
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :id_token_ttl, "ID Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
<%= form.label :id_token_ttl, "ID Token TTL", class: "block text-sm font-medium text-gray-700" %>
|
||||||
<%= form.number_field :id_token_ttl, value: application.id_token_ttl || 3600, min: 300, max: 86400, step: 60, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
<%= 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" %>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
Range: 5 min - 24 hours
|
Range: 5m - 24h
|
||||||
<br>Default: 1 hour (3600s)
|
<br>Default: 1h
|
||||||
<br>Current: <span class="font-medium"><%= application.id_token_ttl_human || "1 hour" %></span>
|
<% if application.id_token_ttl.present? %>
|
||||||
|
<br>Current: <span class="font-medium"><%= application.id_token_ttl_human %> (<%= application.id_token_ttl %>s)</span>
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="mt-3">
|
<details class="mt-3">
|
||||||
<summary class="cursor-pointer text-sm text-blue-600 hover:text-blue-800">Understanding Token Types</summary>
|
<summary class="cursor-pointer text-sm text-blue-600 hover:text-blue-800">Understanding Token Types & Session Length</summary>
|
||||||
<div class="mt-2 ml-4 space-y-2 text-sm text-gray-600">
|
<div class="mt-2 ml-4 space-y-3 text-sm text-gray-600">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-900 mb-1">Token Types:</p>
|
||||||
<p><strong>Access Token:</strong> Used to access protected resources (APIs). Shorter lifetime = more secure. Users won't notice automatic refreshes.</p>
|
<p><strong>Access Token:</strong> Used to access protected resources (APIs). Shorter lifetime = more secure. Users won't notice automatic refreshes.</p>
|
||||||
<p><strong>Refresh Token:</strong> Used to get new access tokens without re-authentication. Longer lifetime = better UX (less re-logins).</p>
|
<p><strong>Refresh Token:</strong> Used to get new access tokens without re-authentication. Each refresh issues a new refresh token (token rotation).</p>
|
||||||
<p><strong>ID Token:</strong> Contains user identity information (JWT). Should match access token lifetime in most cases.</p>
|
<p><strong>ID Token:</strong> Contains user identity information (JWT). Should match access token lifetime in most cases.</p>
|
||||||
<p class="text-xs italic mt-2">💡 Tip: Banking apps use 5-15 min access tokens. Internal tools use 1-4 hours.</p>
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-2">
|
||||||
|
<p class="font-medium text-gray-900 mb-1">How Session Length Works:</p>
|
||||||
|
<p><strong>Refresh Token TTL = Maximum Inactivity Period</strong></p>
|
||||||
|
<p class="ml-3">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 <em>inactive</em> before requiring re-authentication.</p>
|
||||||
|
|
||||||
|
<p class="mt-2"><strong>Example:</strong> Refresh TTL = 30 days</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-1 text-xs">
|
||||||
|
<li>User logs in on Day 0, uses app daily → stays logged in forever (tokens keep rotating)</li>
|
||||||
|
<li>User logs in on Day 0, stops using app → must re-login after 30 days of inactivity</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-2">
|
||||||
|
<p class="font-medium text-gray-900 mb-1">Forcing Re-Authentication:</p>
|
||||||
|
<p class="ml-3 text-xs">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.</p>
|
||||||
|
|
||||||
|
<p class="mt-2 ml-3 text-xs"><strong>To enforce absolute session limits:</strong> Clients can include the <code class="bg-gray-100 px-1 rounded">max_age</code> parameter in their authorization requests to require re-authentication after a specific time, regardless of token rotation.</p>
|
||||||
|
|
||||||
|
<p class="mt-2 ml-3 text-xs"><strong>Example:</strong> A banking app might set <code class="bg-gray-100 px-1 rounded">max_age=900</code> (15 minutes) in the authorization request to force re-authentication every 15 minutes, even if refresh tokens are still valid.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-2">
|
||||||
|
<p class="font-medium text-gray-900 mb-1">Common Configurations:</p>
|
||||||
|
<ul class="ml-3 space-y-1 text-xs">
|
||||||
|
<li><strong>Banking/High Security:</strong> Access TTL = <code class="bg-gray-100 px-1 rounded">5m</code>, Refresh TTL = <code class="bg-gray-100 px-1 rounded">5m</code> → Re-auth every 5 minutes</li>
|
||||||
|
<li><strong>Corporate Tools:</strong> Access TTL = <code class="bg-gray-100 px-1 rounded">1h</code>, Refresh TTL = <code class="bg-gray-100 px-1 rounded">8h</code> → Re-auth after 8 hours inactive</li>
|
||||||
|
<li><strong>Personal Apps:</strong> Access TTL = <code class="bg-gray-100 px-1 rounded">1h</code>, Refresh TTL = <code class="bg-gray-100 px-1 rounded">30d</code> → Re-auth after 30 days inactive</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,9 +147,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if app.user_has_active_session?(@user) %>
|
<% if app.user_has_active_session?(@user) %>
|
||||||
<%= button_to "Logout", logout_from_app_active_sessions_path(application_id: app.id), method: :delete,
|
<%= button_to "Require Re-Auth", logout_from_app_active_sessions_path(application_id: app.id), method: :delete,
|
||||||
class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 transition",
|
class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 transition",
|
||||||
form: { data: { turbo_confirm: "This will log you out of #{app.name}. You can sign back in without re-authorizing. Continue?" } } %>
|
form: { data: { turbo_confirm: "This will revoke #{app.name}'s access tokens. The next time #{app.name} needs to authenticate, you'll sign in again (no re-authorization needed). Continue?" } } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ module Api
|
|||||||
rd: evil_url # Ensure the rd parameter is preserved in login
|
rd: evil_url # Ensure the rd parameter is preserved in login
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_response 302
|
assert_response 303
|
||||||
# Should NOT redirect to evil URL after successful authentication
|
# Should NOT redirect to evil URL after successful authentication
|
||||||
refute_match evil_url, response.location, "Should not redirect to evil URL after authentication"
|
refute_match evil_url, response.location, "Should not redirect to evil URL after authentication"
|
||||||
# Should redirect to the legitimate URL (not the evil one)
|
# Should redirect to the legitimate URL (not the evil one)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
|
|||||||
# Step 3: Sign in
|
# Step 3: Sign in
|
||||||
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
||||||
|
|
||||||
assert_response 302
|
assert_response 303
|
||||||
redirect_uri = URI.parse(response.location)
|
redirect_uri = URI.parse(response.location)
|
||||||
assert_equal "https", redirect_uri.scheme
|
assert_equal "https", redirect_uri.scheme
|
||||||
assert_equal "app.example.com", redirect_uri.host
|
assert_equal "app.example.com", redirect_uri.host
|
||||||
@@ -101,7 +101,7 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Sign in
|
# Sign in
|
||||||
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
||||||
assert_response 302
|
assert_response 303
|
||||||
|
|
||||||
# Should have access (in allowed group)
|
# Should have access (in allowed group)
|
||||||
get "/api/verify", headers: {"X-Forwarded-Host" => "admin.example.com"}
|
get "/api/verify", headers: {"X-Forwarded-Host" => "admin.example.com"}
|
||||||
@@ -139,7 +139,7 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Sign in
|
# Sign in
|
||||||
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
||||||
assert_response 302
|
assert_response 303
|
||||||
|
|
||||||
# Should have access (bypass mode)
|
# Should have access (bypass mode)
|
||||||
get "/api/verify", headers: {"X-Forwarded-Host" => "public.example.com"}
|
get "/api/verify", headers: {"X-Forwarded-Host" => "public.example.com"}
|
||||||
@@ -255,7 +255,7 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Sign in once
|
# Sign in once
|
||||||
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
||||||
assert_response 302
|
assert_response 303
|
||||||
|
|
||||||
# Test access to each application
|
# Test access to each application
|
||||||
apps.each do |app|
|
apps.each do |app|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Step 2: Sign in
|
# Step 2: Sign in
|
||||||
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
post "/signin", params: {email_address: @user.email_address, password: "password"}
|
||||||
assert_response 302
|
assert_response 303
|
||||||
# Signin now redirects back with fa_token parameter
|
# Signin now redirects back with fa_token parameter
|
||||||
assert_match(/\?fa_token=/, response.location)
|
assert_match(/\?fa_token=/, response.location)
|
||||||
assert cookies[:session_id]
|
assert cookies[:session_id]
|
||||||
|
|||||||
Reference in New Issue
Block a user