rails_simple_auth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/MIT-LICENSE +21 -0
- data/README.md +276 -0
- data/app/controllers/rails_simple_auth/base_controller.rb +16 -0
- data/app/controllers/rails_simple_auth/confirmations_controller.rb +45 -0
- data/app/controllers/rails_simple_auth/omniauth_callbacks_controller.rb +32 -0
- data/app/controllers/rails_simple_auth/passwords_controller.rb +66 -0
- data/app/controllers/rails_simple_auth/registrations_controller.rb +52 -0
- data/app/controllers/rails_simple_auth/sessions_controller.rb +90 -0
- data/app/mailers/rails_simple_auth/auth_mailer.rb +46 -0
- data/app/views/rails_simple_auth/confirmations/new.html.erb +27 -0
- data/app/views/rails_simple_auth/mailers/confirmation.html.erb +28 -0
- data/app/views/rails_simple_auth/mailers/magic_link.html.erb +28 -0
- data/app/views/rails_simple_auth/mailers/password_reset.html.erb +28 -0
- data/app/views/rails_simple_auth/passwords/edit.html.erb +33 -0
- data/app/views/rails_simple_auth/passwords/new.html.erb +27 -0
- data/app/views/rails_simple_auth/registrations/new.html.erb +55 -0
- data/app/views/rails_simple_auth/sessions/magic_link_form.html.erb +27 -0
- data/app/views/rails_simple_auth/sessions/new.html.erb +59 -0
- data/lib/generators/rails_simple_auth/css/css_generator.rb +36 -0
- data/lib/generators/rails_simple_auth/css/templates/rails_simple_auth.css +344 -0
- data/lib/generators/rails_simple_auth/install/install_generator.rb +65 -0
- data/lib/generators/rails_simple_auth/install/templates/initializer.rb +106 -0
- data/lib/generators/rails_simple_auth/install/templates/migration.rb +42 -0
- data/lib/generators/rails_simple_auth/views/views_generator.rb +39 -0
- data/lib/rails_simple_auth/configuration.rb +96 -0
- data/lib/rails_simple_auth/controllers/concerns/authentication.rb +104 -0
- data/lib/rails_simple_auth/controllers/concerns/session_management.rb +74 -0
- data/lib/rails_simple_auth/engine.rb +18 -0
- data/lib/rails_simple_auth/models/concerns/authenticatable.rb +65 -0
- data/lib/rails_simple_auth/models/concerns/confirmable.rb +42 -0
- data/lib/rails_simple_auth/models/concerns/magic_linkable.rb +21 -0
- data/lib/rails_simple_auth/models/concerns/oauth_connectable.rb +101 -0
- data/lib/rails_simple_auth/models/current.rb +7 -0
- data/lib/rails_simple_auth/models/session.rb +22 -0
- data/lib/rails_simple_auth/routes.rb +46 -0
- data/lib/rails_simple_auth/version.rb +5 -0
- data/lib/rails_simple_auth.rb +39 -0
- metadata +117 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9da9b2ca95ab7bc4d65390921ab753bfc4181efc88c28ed745bebae4268310a6
|
|
4
|
+
data.tar.gz: 0bf68d88f90560c275c4f680b5c741cb458badc4a328d1fd24ae8ff854834a87
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6099797336414bc988d390ced5d6bb73f8def1c1ac57cee123d86c12945c2784b613b3e476c49ac7334de115a5ae53338da0107282cbd9cc6b0e2a4d01d2710d
|
|
7
|
+
data.tar.gz: 5f676fbf88a8b574cadaa43c96bc52c9e928c966b1d187f31b826c360d1472b160ed473e11582cfa84e232a242275e2ce2fb06714ba9d0a98501734023f94399
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2025-01-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release extracted from [Writero](https://github.com/ivankuznetsov/writero)
|
|
15
|
+
- Session-based authentication with `has_secure_password`
|
|
16
|
+
- Magic link authentication (passwordless login via email)
|
|
17
|
+
- Email confirmation flow for new registrations
|
|
18
|
+
- Password reset flow with secure tokens
|
|
19
|
+
- OAuth support (Google, GitHub) via OmniAuth
|
|
20
|
+
- Rate limiting on authentication endpoints (Rails 8 rate_limit DSL)
|
|
21
|
+
- Configurable callbacks (`after_sign_in_callback`, `after_sign_out_callback`, etc.)
|
|
22
|
+
- Configurable redirect paths (`after_sign_in_path`, `after_sign_out_path`, etc.)
|
|
23
|
+
- Views generator (`rails g rails_simple_auth:views`) for customization
|
|
24
|
+
- CSS generator (`rails g rails_simple_auth:css`) for styling
|
|
25
|
+
- Install generator (`rails g rails_simple_auth:install`) for setup
|
|
26
|
+
- Session management with automatic cleanup of expired sessions
|
|
27
|
+
- Current user tracking via `RailsSimpleAuth::Current`
|
|
28
|
+
- Comprehensive security measures:
|
|
29
|
+
- Open redirect prevention
|
|
30
|
+
- Configurable OAuth account linking
|
|
31
|
+
- Secure signed tokens for password reset and magic links
|
|
32
|
+
- Session invalidation on password change
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ivan Kuznetsov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# RailsSimpleAuth
|
|
2
|
+
|
|
3
|
+
Simple, secure authentication for Rails 8+ applications. Built on Rails primitives with no magic.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Email/Password authentication** with bcrypt
|
|
8
|
+
- **Magic link** (passwordless) authentication
|
|
9
|
+
- **Email confirmation** with signed tokens
|
|
10
|
+
- **Password reset** with signed tokens
|
|
11
|
+
- **OAuth support** (Google, GitHub, etc.)
|
|
12
|
+
- **Rate limiting** built-in
|
|
13
|
+
- **Session tracking** with IP and user agent
|
|
14
|
+
- **Customizable styling** via CSS variables
|
|
15
|
+
- **No dependencies** beyond Rails and bcrypt
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add to your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "rails_simple_auth"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run the installer:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rails generate rails_simple_auth:install
|
|
35
|
+
rails db:migrate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Add concerns to your User model:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
class User < ApplicationRecord
|
|
42
|
+
include RailsSimpleAuth::Models::Concerns::Authenticatable
|
|
43
|
+
include RailsSimpleAuth::Models::Concerns::Confirmable # optional
|
|
44
|
+
include RailsSimpleAuth::Models::Concerns::MagicLinkable # optional
|
|
45
|
+
include RailsSimpleAuth::Models::Concerns::OAuthConnectable # optional
|
|
46
|
+
|
|
47
|
+
# Your custom fields and validations
|
|
48
|
+
validates :company_name, presence: true
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Protect your routes:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class ApplicationController < ActionController::Base
|
|
56
|
+
before_action :require_authentication
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## User Model Customization
|
|
61
|
+
|
|
62
|
+
The gem doesn't own your User model—you do. Add any custom fields:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# db/migrate/xxx_create_users.rb
|
|
66
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
|
67
|
+
def change
|
|
68
|
+
create_table :users do |t|
|
|
69
|
+
# Required by gem
|
|
70
|
+
t.string :email_address, null: false
|
|
71
|
+
t.string :password_digest, null: false
|
|
72
|
+
t.datetime :confirmed_at # if using Confirmable
|
|
73
|
+
|
|
74
|
+
# Your custom fields
|
|
75
|
+
t.string :name
|
|
76
|
+
t.string :company_name
|
|
77
|
+
t.boolean :admin, default: false
|
|
78
|
+
t.string :oauth_provider
|
|
79
|
+
t.string :oauth_uid
|
|
80
|
+
|
|
81
|
+
t.timestamps
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
add_index :users, :email_address, unique: true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Styling
|
|
90
|
+
|
|
91
|
+
The gem ships with **no CSS by default** (Option B). Generate base styles:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
rails generate rails_simple_auth:css
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then customize by overriding CSS variables:
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
/* In your application.css */
|
|
101
|
+
:root {
|
|
102
|
+
--rsa-color-primary: #22c55e; /* Your brand color */
|
|
103
|
+
--rsa-color-background-form: #f0fdf4; /* Form background */
|
|
104
|
+
--rsa-color-text: #166534; /* Text color */
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or edit `rails_simple_auth.css` directly for complete control.
|
|
109
|
+
|
|
110
|
+
### CSS Variables Reference
|
|
111
|
+
|
|
112
|
+
| Variable | Default | Description |
|
|
113
|
+
|----------|---------|-------------|
|
|
114
|
+
| `--rsa-color-primary` | `#3b82f6` | Primary button/link color |
|
|
115
|
+
| `--rsa-color-primary-hover` | `#2563eb` | Primary hover state |
|
|
116
|
+
| `--rsa-color-background-form` | `#ffffff` | Form container background |
|
|
117
|
+
| `--rsa-color-text` | `#374151` | Main text color |
|
|
118
|
+
| `--rsa-color-text-muted` | `#6b7280` | Secondary text color |
|
|
119
|
+
| `--rsa-color-border` | `#e5e7eb` | Border color |
|
|
120
|
+
| `--rsa-color-danger` | `#dc2626` | Error message color |
|
|
121
|
+
|
|
122
|
+
## View Customization
|
|
123
|
+
|
|
124
|
+
Copy views for full customization:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
rails generate rails_simple_auth:views
|
|
128
|
+
|
|
129
|
+
# Or specific views only
|
|
130
|
+
rails generate rails_simple_auth:views --only sessions passwords
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Views use BEM naming: `.rsa-auth-form`, `.rsa-auth-form__input`, etc.
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# config/initializers/rails_simple_auth.rb
|
|
139
|
+
RailsSimpleAuth.configure do |config|
|
|
140
|
+
# Features
|
|
141
|
+
config.magic_link_enabled = true
|
|
142
|
+
config.email_confirmation_enabled = true
|
|
143
|
+
config.enable_oauth(:google, :github)
|
|
144
|
+
|
|
145
|
+
# Token expiration
|
|
146
|
+
config.magic_link_expiry = 15.minutes
|
|
147
|
+
config.password_reset_expiry = 15.minutes
|
|
148
|
+
config.confirmation_expiry = 24.hours
|
|
149
|
+
|
|
150
|
+
# Paths (symbol, string, or proc)
|
|
151
|
+
config.after_sign_in_path = :dashboard_path
|
|
152
|
+
config.after_sign_out_path = -> { new_session_path }
|
|
153
|
+
|
|
154
|
+
# Layout
|
|
155
|
+
config.layout = "auth" # Use a custom layout
|
|
156
|
+
|
|
157
|
+
# Mailer
|
|
158
|
+
config.mailer_sender = "auth@myapp.com"
|
|
159
|
+
|
|
160
|
+
# Password requirements
|
|
161
|
+
config.password_minimum_length = 12
|
|
162
|
+
|
|
163
|
+
# Callbacks
|
|
164
|
+
config.after_sign_in_callback = ->(user, controller) {
|
|
165
|
+
Analytics.track("sign_in", user_id: user.id)
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## OAuth Setup
|
|
171
|
+
|
|
172
|
+
1. Enable providers:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
RailsSimpleAuth.configure do |config|
|
|
176
|
+
config.enable_oauth(:google, :github)
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
2. Configure OmniAuth:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# config/initializers/omniauth.rb
|
|
184
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
185
|
+
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
|
|
186
|
+
provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
3. Optionally map OAuth fields:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
class User < ApplicationRecord
|
|
194
|
+
include RailsSimpleAuth::Models::Concerns::OAuthConnectable
|
|
195
|
+
|
|
196
|
+
def assign_oauth_attributes(auth_hash)
|
|
197
|
+
self.name = auth_hash.dig("info", "name")
|
|
198
|
+
self.avatar_url = auth_hash.dig("info", "image")
|
|
199
|
+
self.oauth_provider = auth_hash["provider"]
|
|
200
|
+
self.oauth_uid = auth_hash["uid"]
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Controller Customization
|
|
206
|
+
|
|
207
|
+
Subclass controllers for custom behavior:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# app/controllers/sessions_controller.rb
|
|
211
|
+
class SessionsController < RailsSimpleAuth::SessionsController
|
|
212
|
+
def after_sign_in(user)
|
|
213
|
+
track_login(user)
|
|
214
|
+
super
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Update routes to use your controller:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
rails_simple_auth_routes(sessions_controller: "sessions")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Helpers
|
|
226
|
+
|
|
227
|
+
Available in controllers and views:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
current_user # The signed-in user (or nil)
|
|
231
|
+
user_signed_in? # Boolean
|
|
232
|
+
require_authentication # Redirects if not signed in
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Access anywhere via:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
RailsSimpleAuth::Current.user
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Routes
|
|
242
|
+
|
|
243
|
+
The gem adds these routes:
|
|
244
|
+
|
|
245
|
+
| Method | Path | Description |
|
|
246
|
+
|--------|------|-------------|
|
|
247
|
+
| GET | `/session/new` | Sign in form |
|
|
248
|
+
| POST | `/session` | Create session |
|
|
249
|
+
| DELETE | `/session` | Sign out |
|
|
250
|
+
| GET | `/sign_up` | Sign up form |
|
|
251
|
+
| POST | `/sign_up` | Create account |
|
|
252
|
+
| GET | `/passwords/new` | Password reset form |
|
|
253
|
+
| POST | `/passwords` | Send reset email |
|
|
254
|
+
| GET | `/passwords/:token/edit` | New password form |
|
|
255
|
+
| PATCH | `/passwords/:token` | Update password |
|
|
256
|
+
| GET | `/confirmations/new` | Resend confirmation |
|
|
257
|
+
| POST | `/confirmations` | Send confirmation |
|
|
258
|
+
| GET | `/confirmations/:token` | Confirm email |
|
|
259
|
+
| GET | `/magic_link_form` | Magic link form |
|
|
260
|
+
| POST | `/request_magic_link` | Send magic link |
|
|
261
|
+
| GET | `/magic_link` | Login via magic link |
|
|
262
|
+
|
|
263
|
+
## Security Features
|
|
264
|
+
|
|
265
|
+
- **BCrypt password hashing** with salts
|
|
266
|
+
- **Constant-time comparison** prevents timing attacks
|
|
267
|
+
- **Signed tokens** for all email links
|
|
268
|
+
- **Rate limiting** on all auth endpoints
|
|
269
|
+
- **HttpOnly cookies** for session tokens
|
|
270
|
+
- **SameSite=Lax** CSRF protection
|
|
271
|
+
- **Session invalidation** on password change
|
|
272
|
+
- **IP and user agent tracking** for audit
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT License
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class BaseController < ::ApplicationController
|
|
5
|
+
include RailsSimpleAuth::Controllers::Concerns::Authentication
|
|
6
|
+
include RailsSimpleAuth::Controllers::Concerns::SessionManagement
|
|
7
|
+
|
|
8
|
+
layout -> { RailsSimpleAuth.configuration.layout }
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def user_class
|
|
13
|
+
RailsSimpleAuth.configuration.user_class
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class ConfirmationsController < BaseController
|
|
5
|
+
skip_before_action :require_authentication, only: %i[new create show], raise: false
|
|
6
|
+
|
|
7
|
+
unless Rails.env.local?
|
|
8
|
+
rate_limit to: 3, within: 1.hour, by: -> { client_ip }, only: :create,
|
|
9
|
+
with: -> { redirect_to new_confirmation_path, alert: "Too many confirmation requests. Please try again later." }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
user = user_class.find_by_email(params[:email_address])
|
|
17
|
+
|
|
18
|
+
if user && user.respond_to?(:unconfirmed?) && user.unconfirmed?
|
|
19
|
+
token = user.generate_confirmation_token
|
|
20
|
+
RailsSimpleAuth.configuration.mailer.confirmation(user, token).deliver_later
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
redirect_to new_session_path, notice: "If an unconfirmed account exists with that email, confirmation instructions have been sent."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
user = user_class.find_signed(params[:token], purpose: :email_confirmation)
|
|
28
|
+
|
|
29
|
+
if user
|
|
30
|
+
user.confirm! if user.respond_to?(:confirm!)
|
|
31
|
+
run_after_confirmation_callback(user)
|
|
32
|
+
redirect_to resolve_path(:after_confirmation_path), notice: "Email confirmed! You can now sign in."
|
|
33
|
+
else
|
|
34
|
+
redirect_to new_confirmation_path, alert: "Invalid or expired confirmation link."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_after_confirmation_callback(user)
|
|
41
|
+
callback = RailsSimpleAuth.configuration.after_confirmation_callback
|
|
42
|
+
callback&.call(user, self)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class OmniauthCallbacksController < BaseController
|
|
5
|
+
skip_before_action :require_authentication, raise: false
|
|
6
|
+
skip_before_action :verify_authenticity_token, only: :create
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
auth_hash = request.env["omniauth.auth"]
|
|
10
|
+
provider = params[:provider]
|
|
11
|
+
|
|
12
|
+
unless RailsSimpleAuth.configuration.oauth_provider_enabled?(provider)
|
|
13
|
+
redirect_to new_session_path, alert: "OAuth provider not enabled."
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
user = user_class.from_oauth(auth_hash)
|
|
18
|
+
|
|
19
|
+
if user&.persisted?
|
|
20
|
+
create_session_for(user)
|
|
21
|
+
run_after_sign_in_callback(user)
|
|
22
|
+
redirect_to resolve_path(:after_sign_in_path), notice: "Signed in successfully with #{provider.to_s.capitalize}."
|
|
23
|
+
else
|
|
24
|
+
redirect_to new_session_path, alert: "Could not authenticate with #{provider.to_s.capitalize}."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def failure
|
|
29
|
+
redirect_to new_session_path, alert: "Authentication failed. Please try again."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class PasswordsController < BaseController
|
|
5
|
+
skip_before_action :require_authentication, only: %i[new create edit update], raise: false
|
|
6
|
+
before_action :set_user_from_token, only: %i[edit update]
|
|
7
|
+
|
|
8
|
+
unless Rails.env.local?
|
|
9
|
+
rate_limit to: 3, within: 1.hour, by: -> { client_ip }, only: :create,
|
|
10
|
+
with: -> { redirect_to new_password_path, alert: "Too many password reset requests. Please try again later." }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
user = user_class.find_by_email(params[:email_address])
|
|
18
|
+
|
|
19
|
+
if user && can_reset_password?(user)
|
|
20
|
+
token = user.generate_password_reset_token
|
|
21
|
+
RailsSimpleAuth.configuration.mailer.password_reset(user, token).deliver_later
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
redirect_to new_session_path, notice: "If an account exists with that email, password reset instructions have been sent."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def edit
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
ActiveRecord::Base.transaction do
|
|
32
|
+
if @user.update(password_params)
|
|
33
|
+
@user.invalidate_all_sessions!
|
|
34
|
+
redirect_to new_session_path, notice: "Password has been reset. Please sign in with your new password."
|
|
35
|
+
else
|
|
36
|
+
render :edit, status: :unprocessable_content
|
|
37
|
+
raise ActiveRecord::Rollback
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
41
|
+
Rails.logger.error(
|
|
42
|
+
"[RailsSimpleAuth] Session invalidation failed after password reset for user #{@user.id}: #{e.message}"
|
|
43
|
+
)
|
|
44
|
+
# Password was rolled back due to transaction, redirect with error
|
|
45
|
+
redirect_to new_password_path, alert: "Password reset failed. Please try again."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_user_from_token
|
|
51
|
+
@user = user_class.find_signed(params[:token], purpose: :password_reset)
|
|
52
|
+
redirect_to new_password_path, alert: "Invalid or expired password reset link." unless @user
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def can_reset_password?(user)
|
|
56
|
+
return true unless RailsSimpleAuth.configuration.email_confirmation_enabled
|
|
57
|
+
return true unless user.respond_to?(:confirmed?)
|
|
58
|
+
|
|
59
|
+
user.confirmed?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def password_params
|
|
63
|
+
params.require(:user).permit(:password, :password_confirmation)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class RegistrationsController < BaseController
|
|
5
|
+
skip_before_action :require_authentication, only: %i[new create], raise: false
|
|
6
|
+
|
|
7
|
+
unless Rails.env.local?
|
|
8
|
+
rate_limit to: 5, within: 1.hour, by: -> { client_ip }, only: :create,
|
|
9
|
+
with: -> { redirect_to sign_up_path, alert: "Too many sign up attempts. Please try again later." }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
|
|
14
|
+
@user = user_class.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
@user = user_class.new(registration_params)
|
|
19
|
+
|
|
20
|
+
if @user.save
|
|
21
|
+
after_successful_registration
|
|
22
|
+
else
|
|
23
|
+
render :new, status: :unprocessable_content
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def registration_params
|
|
30
|
+
params.require(:user).permit(:email_address, :password)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def after_successful_registration
|
|
34
|
+
if RailsSimpleAuth.configuration.email_confirmation_enabled
|
|
35
|
+
send_confirmation_email(@user)
|
|
36
|
+
run_after_sign_up_callback(@user)
|
|
37
|
+
redirect_to new_session_path, notice: "Account created! Please check your email to confirm your account."
|
|
38
|
+
else
|
|
39
|
+
create_session_for(@user)
|
|
40
|
+
run_after_sign_up_callback(@user)
|
|
41
|
+
redirect_to resolve_path(:after_sign_up_path), notice: "Account created successfully!"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_confirmation_email(user)
|
|
46
|
+
return unless user.respond_to?(:generate_confirmation_token)
|
|
47
|
+
|
|
48
|
+
token = user.generate_confirmation_token
|
|
49
|
+
RailsSimpleAuth.configuration.mailer.confirmation(user, token).deliver_later
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class SessionsController < BaseController
|
|
5
|
+
skip_before_action :require_authentication,
|
|
6
|
+
only: %i[new create magic_link_form request_magic_link magic_link_login],
|
|
7
|
+
raise: false
|
|
8
|
+
|
|
9
|
+
unless Rails.env.local?
|
|
10
|
+
rate_limit to: 5, within: 15.minutes, by: -> { client_ip }, only: :create,
|
|
11
|
+
with: -> { redirect_to new_session_path, alert: "Too many login attempts. Please try again later." }
|
|
12
|
+
|
|
13
|
+
rate_limit to: 3, within: 10.minutes, by: -> { params[:email_address].to_s.downcase }, only: :request_magic_link,
|
|
14
|
+
with: -> { redirect_to new_session_path, alert: "Too many magic link requests. Please try again later." }
|
|
15
|
+
|
|
16
|
+
rate_limit to: 5, within: 15.minutes, by: -> { client_ip }, only: :magic_link_login,
|
|
17
|
+
with: -> { redirect_to new_session_path, alert: "Too many magic link attempts. Please try again later." }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new
|
|
21
|
+
redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create
|
|
25
|
+
user = user_class.find_by_email(params[:email_address]) || user_class.new(password: SecureRandom.hex(32))
|
|
26
|
+
|
|
27
|
+
if user.authenticate(params[:password]) && user.persisted?
|
|
28
|
+
if confirmation_required_for?(user)
|
|
29
|
+
@error_message = "Please confirm your email before signing in."
|
|
30
|
+
@previous_email = params[:email_address]
|
|
31
|
+
render :new, status: :unprocessable_content
|
|
32
|
+
else
|
|
33
|
+
sign_in_and_redirect(user)
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
Rails.logger.warn("Failed login attempt for email: #{params[:email_address]} from IP: #{client_ip}")
|
|
37
|
+
@error_message = "Invalid email or password"
|
|
38
|
+
@previous_email = params[:email_address]
|
|
39
|
+
render :new, status: :unprocessable_content
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy
|
|
44
|
+
user = current_user
|
|
45
|
+
destroy_current_session
|
|
46
|
+
run_after_sign_out_callback(user) if user
|
|
47
|
+
redirect_to resolve_path(:after_sign_out_path), notice: "Signed out successfully."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def magic_link_form
|
|
51
|
+
redirect_to resolve_path(:after_sign_in_path) if user_signed_in?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def request_magic_link
|
|
55
|
+
user = user_class.find_by_email(params[:email_address])
|
|
56
|
+
|
|
57
|
+
if user && user.respond_to?(:generate_magic_link_token)
|
|
58
|
+
token = user.generate_magic_link_token
|
|
59
|
+
RailsSimpleAuth.configuration.mailer.magic_link(user, token).deliver_later
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
redirect_to new_session_path, notice: "If an account exists with that email, a magic link has been sent."
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def magic_link_login
|
|
66
|
+
user = user_class.find_signed(params[:token], purpose: :magic_link)
|
|
67
|
+
|
|
68
|
+
if user
|
|
69
|
+
user.confirm! if user.respond_to?(:confirm!) && user.respond_to?(:unconfirmed?) && user.unconfirmed?
|
|
70
|
+
sign_in_and_redirect(user)
|
|
71
|
+
else
|
|
72
|
+
redirect_to new_session_path, alert: "Invalid or expired magic link."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def confirmation_required_for?(user)
|
|
79
|
+
RailsSimpleAuth.configuration.email_confirmation_enabled &&
|
|
80
|
+
user.respond_to?(:unconfirmed?) &&
|
|
81
|
+
user.unconfirmed?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sign_in_and_redirect(user)
|
|
85
|
+
create_session_for(user)
|
|
86
|
+
run_after_sign_in_callback(user)
|
|
87
|
+
redirect_to stored_location_or_default, notice: "Signed in successfully."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleAuth
|
|
4
|
+
class AuthMailer < ActionMailer::Base
|
|
5
|
+
default from: -> { RailsSimpleAuth.configuration.mailer_sender }
|
|
6
|
+
|
|
7
|
+
def confirmation(user, token)
|
|
8
|
+
@user = user
|
|
9
|
+
@token = token
|
|
10
|
+
@confirmation_url = main_app.confirmation_url(token: token)
|
|
11
|
+
|
|
12
|
+
mail(
|
|
13
|
+
to: user.email_address,
|
|
14
|
+
subject: "Confirm your email"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def magic_link(user, token)
|
|
19
|
+
@user = user
|
|
20
|
+
@token = token
|
|
21
|
+
@magic_link_url = main_app.magic_link_url(token: token)
|
|
22
|
+
|
|
23
|
+
mail(
|
|
24
|
+
to: user.email_address,
|
|
25
|
+
subject: "Sign in to your account"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def password_reset(user, token)
|
|
30
|
+
@user = user
|
|
31
|
+
@token = token
|
|
32
|
+
@password_reset_url = main_app.edit_password_url(token: token)
|
|
33
|
+
|
|
34
|
+
mail(
|
|
35
|
+
to: user.email_address,
|
|
36
|
+
subject: "Reset your password"
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def main_app
|
|
43
|
+
Rails.application.routes.url_helpers
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|