diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f915c45..deeb1cc 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -2,6 +2,7 @@ class ProfilesController < ApplicationController def show @user = Current.session.user @active_sessions = @user.sessions.active.order(last_activity_at: :desc) + @connected_applications = @user.oidc_user_consents.includes(:application).order(granted_at: :desc) end def update @@ -33,6 +34,34 @@ class ProfilesController < ApplicationController end end + def revoke_consent + @user = Current.session.user + application = Application.find(params[:application_id]) + + # Check if user has consent for this application + consent = @user.oidc_user_consents.find_by(application: application) + unless consent + redirect_to profile_path, alert: "No consent found for this application." + return + end + + # Revoke the consent + consent.destroy + redirect_to profile_path, notice: "Successfully revoked access to #{application.name}." + end + + def revoke_all_consents + @user = Current.session.user + count = @user.oidc_user_consents.count + + if count > 0 + @user.oidc_user_consents.destroy_all + redirect_to profile_path, notice: "Successfully revoked access to #{count} applications." + else + redirect_to profile_path, alert: "No applications to revoke." + end + end + private def email_params diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d5b9973..26da98a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,7 +1,7 @@ class SessionsController < ApplicationController allow_unauthenticated_access only: %i[ new create verify_totp ] - rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." } - rate_limit to: 5, within: 3.minutes, only: :verify_totp, with: -> { redirect_to totp_verification_path, alert: "Too many attempts. Try again later." } + rate_limit to: 20, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." } + rate_limit to: 10, within: 3.minutes, only: :verify_totp, with: -> { redirect_to totp_verification_path, alert: "Too many attempts. Try again later." } def new # Redirect to signup if this is first run diff --git a/app/models/oidc_user_consent.rb b/app/models/oidc_user_consent.rb index c1c47c8..07ced38 100644 --- a/app/models/oidc_user_consent.rb +++ b/app/models/oidc_user_consent.rb @@ -26,6 +26,24 @@ class OidcUserConsent < ApplicationRecord (requested - granted).empty? end + # Get a human-readable list of scopes + def formatted_scopes + scopes.map do |scope| + case scope + when 'openid' + 'Basic authentication' + when 'profile' + 'Profile information' + when 'email' + 'Email address' + when 'groups' + 'Group membership' + else + scope.humanize + end + end.join(', ') + end + private def set_granted_at diff --git a/app/models/user.rb b/app/models/user.rb index 2417d26..30842e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -80,6 +80,15 @@ class User < ApplicationRecord .find { |consent| consent.covers_scopes?(requested_scopes) } end + def revoke_consent!(application) + consent = oidc_user_consents.find_by(application: application) + consent&.destroy + end + + def revoke_all_consents! + oidc_user_consents.destroy_all + end + private def generate_backup_codes diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 406b488..58a283d 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -1,7 +1,7 @@
-

Profile & Settings

-

Manage your account settings and security preferences.

+

Account Security

+

Manage your account settings, active sessions, and connected applications.

@@ -199,6 +199,44 @@ } + +
+
+

Connected Applications

+
+

These applications have access to your account. You can revoke access at any time.

+
+
+ <% if @connected_applications.any? %> +
    + <% @connected_applications.each do |consent| %> +
  • +
    +
    +

    + <%= consent.application.name %> +

    +

    + Access to: <%= consent.formatted_scopes %> +

    +

    + Authorized <%= time_ago_in_words(consent.granted_at) %> ago +

    +
    + <%= button_to "Revoke Access", revoke_consent_profile_path(application_id: consent.application.id), method: :delete, + class: "inline-flex items-center rounded-md border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2", + form: { data: { turbo_confirm: "Are you sure you want to revoke access to #{consent.application.name}? You'll need to re-authorize this application to use it again." } } %> +
    +
  • + <% end %> +
+ <% else %> +

No connected applications.

+ <% end %> +
+
+
+
@@ -243,4 +281,27 @@
+ + +
+
+

Security Actions

+
+

Use these actions to quickly secure your account. Be careful - these actions cannot be undone.

+
+
+ <% if @active_sessions.count > 1 %> + <%= button_to "Sign Out Everywhere Else", session_path(Current.session), method: :delete, + class: "inline-flex items-center rounded-md border border-orange-300 bg-white px-4 py-2 text-sm font-medium text-orange-700 shadow-sm hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2", + form: { data: { turbo_confirm: "This will sign you out from all other devices except this one. Are you sure?" } } %> + <% end %> + + <% if @connected_applications.any? %> + <%= button_to "Revoke All App Access", revoke_all_consents_profile_path, method: :delete, + class: "inline-flex items-center rounded-md border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2", + form: { data: { turbo_confirm: "This will revoke access from all connected applications. You'll need to re-authorize each application to use them again. Are you sure?" } } %> + <% end %> +
+
+
diff --git a/config/routes.rb b/config/routes.rb index 2e6004e..d9cd205 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,7 +34,12 @@ Rails.application.routes.draw do # Authenticated routes root "dashboard#index" - resource :profile, only: [:show, :update] + resource :profile, only: [:show, :update] do + member do + delete :revoke_consent + delete :revoke_all_consents + end + end resources :sessions, only: [] do member do delete :destroy, action: :destroy_other