Class: Admin::SessionsController
- Inherits:
-
ActionController::Base
- Object
- ActionController::Base
- Admin::SessionsController
- Defined in:
- lib/generators/open_loam/install/templates/admin/sessions_controller.rb
Overview
Email + password login for the admin, then (if the user has MFA) a TOTP challenge, then the tenant pick. A OpenLoam user can belong to several tenants, so: authenticate → second factor → choose a tenant where they hold a membership. The second factor runs BEFORE any tenant is chosen, which is why the MFA secret is keyed to the user, not a tenant (OpenLoam::MfaCredential).
Not a BaseController subclass: everything there assumes an established OpenLoam::Current, which is exactly what this controller is trying to build.
Instance Method Summary collapse
- #create ⇒ Object
- #destroy ⇒ Object
-
#mfa_challenge ⇒ Object
The TOTP challenge screen (only reachable mid-login, password already given).
- #mfa_verify ⇒ Object
- #new ⇒ Object
-
#select_tenant ⇒ Object
Step two (or three): the tenant picker posts here.
-
#sso_callback ⇒ Object
The IdP redirects back here with a code + state.
-
#sso_start ⇒ Object
SSO entry: the user types an email, home-realm discovery finds the tenant's IdP by domain, and we redirect to it.
Instance Method Details
#create ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 20 def create email = params[:email] # Rate-limit BEFORE touching the password (OpenLoam::AuthThrottle). Throttle by # the submitted identifier whether or not the account exists, and give the # SAME generic locked response either way — so a lockout can't become an # account-existence oracle. if OpenLoam::AuthThrottle.locked?(email) @error = (email) return render :new, status: :too_many_requests end user = User.authenticate_by(email: email, password: params[:password]) if user.nil? OpenLoam::AuthThrottle.record_failure(email, kind: "password", ip: request.remote_ip) # One message for both cases on purpose: saying which half was wrong # tells an attacker which emails exist. @error = "Wrong email or password." return render :new, status: :unauthorized end # PASSWORD only — clearing every kind here would reset the TOTP counter. OpenLoam::AuthThrottle.clear(email, kind: "password") reset_session # a fresh session id at login: no fixation session[:user_id] = user.id # Password is only the first factor when MFA is active — hold the login in # a "pending" state (no tenant, no access) until the code checks out. if OpenLoam::MfaCredential.active_for(user) session[:mfa_pending] = true redirect_to mfa_challenge_admin_session_path else complete_authentication(user) end end |
#destroy ⇒ Object
173 174 175 176 177 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 173 def destroy reset_session OpenLoam::Current.reset redirect_to new_admin_session_path end |
#mfa_challenge ⇒ Object
The TOTP challenge screen (only reachable mid-login, password already given).
58 59 60 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 58 def mfa_challenge @user = mfa_challenge_user or return redirect_to new_admin_session_path end |
#mfa_verify ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 62 def mfa_verify user = mfa_challenge_user or return redirect_to new_admin_session_path # A 6-digit TOTP (with drift, several codes valid) is the prime brute-force # target — lock it out on the user's identifier. if OpenLoam::AuthThrottle.locked?(user.email) @user = user @error = (user.email) return render :mfa_challenge, status: :too_many_requests end credential = OpenLoam::MfaCredential.active_for(user) if credential.verify_totp(params[:code]) || credential.consume_recovery_code(params[:code]) OpenLoam::AuthThrottle.clear(user.email, kind: "totp") session.delete(:mfa_pending) complete_authentication(user) else OpenLoam::AuthThrottle.record_failure(user.email, kind: "totp", ip: request.remote_ip) # One generic error — never reveal whether a code was close or a # recovery code was already spent. @user = user @error = "That code is not valid." render :mfa_challenge, status: :unauthorized end end |
#new ⇒ Object
13 14 15 16 17 18 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 13 def new return redirect_to mfa_challenge_admin_session_path if session[:mfa_pending] @user = authenticated_user @tenants = @user ? OpenLoam::Membership.tenants_for(@user) : OpenLoam::Tenant.none end |
#select_tenant ⇒ Object
Step two (or three): the tenant picker posts here.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 158 def select_tenant return redirect_to mfa_challenge_admin_session_path if session[:mfa_pending] user = authenticated_user return redirect_to new_admin_session_path if user.nil? tenant = OpenLoam::Membership.tenants_for(user).find_by(id: params[:tenant_id]) # A tenant the user has no membership in is simply not in that list, so # picking one is impossible rather than merely forbidden. return redirect_to new_admin_session_path, alert: "You are not a member of that tenant." if tenant.nil? enter(user, tenant) end |
#sso_callback ⇒ Object
The IdP redirects back here with a code + state. GET, so Rails' form CSRF
does not apply — the state check IS the CSRF defense. Verify it first,
then exchange the code and provision inside the provider's tenant.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 117 def sso_callback state = session.delete(:sso_state) provider_id = session.delete(:sso_provider_id) tenant_id = session.delete(:sso_tenant_id) if state.blank? || !ActiveSupport::SecurityUtils.secure_compare(state, params[:state].to_s) return redirect_to new_admin_session_path, alert: "SSO sign-in could not be verified. Please try again." end tenant = OpenLoam::Tenant.find_by(id: tenant_id) provider = tenant && OpenLoam.as_tenant(tenant) { OpenLoam::SsoProvider.find_by(id: provider_id) } if provider.nil? || !provider.active? return redirect_to new_admin_session_path, alert: "That SSO provider is no longer available." end user = OpenLoam.as_tenant(tenant) do claims = OpenLoam::Sso.build(provider, redirect_uri: sso_callback_admin_session_url).exchange(code: params[:code]) OpenLoam::Sso.provision(provider, claims) end reset_session session[:user_id] = user.id session[:sso_tenant_id] = tenant.id # land in the IdP's tenant after any MFA # SSO establishes primary auth; if the user also runs app-side MFA, still # require the second factor (the safe choice — SSO does not waive it). if OpenLoam::MfaCredential.active_for(user) session[:mfa_pending] = true redirect_to mfa_challenge_admin_session_path else complete_authentication(user) end rescue OpenLoam::Sso::Error # An unverified email, a domain the provider does not own, or any other # provisioning refusal: no session, nothing linked, one generic message. reset_session redirect_to new_admin_session_path, alert: "We couldn't sign you in with SSO. Please contact your administrator." end |
#sso_start ⇒ Object
SSO entry: the user types an email, home-realm discovery finds the tenant's
IdP by domain, and we redirect to it. No provider for that domain? Fall
back to password login. The state is our CSRF token for the round-trip.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/generators/open_loam/install/templates/admin/sessions_controller.rb', line 92 def sso_start email = params[:email].to_s provider = OpenLoam::Sso.provider_for(email: email) if provider.nil? return redirect_to new_admin_session_path(email: email), alert: "No SSO provider is configured for that email domain — sign in with your password." end state = SecureRandom.urlsafe_base64(24) reset_session session[:sso_state] = state session[:sso_provider_id] = provider.id session[:sso_tenant_id] = provider.tenant_id url = OpenLoam.as_tenant(provider.tenant) do OpenLoam::Sso.build(provider, redirect_uri: sso_callback_admin_session_url) .(state: state, login_hint: email) end redirect_to url, allow_other_host: true end |