action_auth 0.1.10 → 0.2.1
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 +4 -4
- data/README.md +46 -6
- data/app/assets/config/action_auth_manifest.js +1 -0
- data/app/assets/javascripts/action_auth/application.js +98 -0
- data/app/controllers/action_auth/sessions_controller.rb +11 -4
- data/app/controllers/action_auth/webauthn_credential_authentications_controller.rb +50 -0
- data/app/controllers/action_auth/webauthn_credentials_controller.rb +57 -0
- data/app/models/action_auth/user.rb +13 -2
- data/app/models/action_auth/webauthn_credential.rb +12 -0
- data/app/views/action_auth/sessions/index.html.erb +36 -0
- data/app/views/action_auth/webauthn_credential_authentications/new.html.erb +12 -0
- data/app/views/action_auth/webauthn_credentials/new.html.erb +21 -0
- data/app/views/layouts/action_auth/application-full-width.html.erb +5 -0
- data/app/views/layouts/action_auth/application.html.erb +1 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20240111125859_add_webauthn_credentials.rb +16 -0
- data/db/migrate/20240111142545_add_webauthn_id_to_users.rb +5 -0
- data/lib/action_auth/configuration.rb +19 -0
- data/lib/action_auth/controllers/helpers.rb +6 -0
- data/lib/action_auth/version.rb +1 -1
- data/lib/action_auth.rb +17 -1
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5dfe867ff71fdec35c9997e465f8cb471a8ea57733373cec378c6117cf643a6b
|
4
|
+
data.tar.gz: 16182c5888a401678b3d2637c3b0396e8730d06d4bef1da260c34b524a3fd296
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2fefbcc78596506ec864843444c196aa4e580b874f73fe53676409dba0d065a57527ae92cdc7a9fc061a3da498b15af24c8618389264dcac3ba32b39461e685
|
7
|
+
data.tar.gz: 78dd9cd3b739ed973c506b94f83d6cbbfa8c228d025376b571a4f4c156a4be80b085fd9fe44ba6823cd687c926b4936869410fc1e7db0adc02ac8dd396668819
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ bundle add action_auth
|
|
15
15
|
bin/rails action_auth:install:migrations
|
16
16
|
```
|
17
17
|
|
18
|
-
Modify config/routes.rb to include the following:
|
18
|
+
Modify config/routes.rb to include the following (note that the path can be anything you want):
|
19
19
|
|
20
20
|
```ruby
|
21
21
|
mount ActionAuth::Engine => 'action_auth'
|
@@ -25,7 +25,7 @@ In your view layout
|
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
<% if user_signed_in? %>
|
28
|
-
<li><%= link_to "
|
28
|
+
<li><%= link_to "Security", user_sessions_path %></li>
|
29
29
|
<li><%= button_to "Sign Out", user_session_path(current_session), method: :delete %></li>
|
30
30
|
<% else %>
|
31
31
|
<li><%= link_to "Sign In", new_user_session_path %></li>
|
@@ -33,6 +33,8 @@ In your view layout
|
|
33
33
|
<% end %>
|
34
34
|
```
|
35
35
|
|
36
|
+
See [WebAuthn](#webauthn) for additional configuration.
|
37
|
+
|
36
38
|
## Features
|
37
39
|
|
38
40
|
These are the planned features for ActionAuth. The ones that are checked off are currently implemented. The ones that are not checked off are planned for future releases.
|
@@ -45,9 +47,11 @@ These are the planned features for ActionAuth. The ones that are checked off are
|
|
45
47
|
|
46
48
|
✅ - Cookie-based sessions
|
47
49
|
|
48
|
-
|
50
|
+
✅ - Device Session Management
|
51
|
+
|
52
|
+
✅ - Multifactor Authentication (through Passkeys)
|
49
53
|
|
50
|
-
|
54
|
+
✅ - Passkeys/Hardware Security Keys
|
51
55
|
|
52
56
|
⏳ - Magic Links
|
53
57
|
|
@@ -127,6 +131,41 @@ versus a user that is not logged in.
|
|
127
131
|
end
|
128
132
|
root to: 'welcome#index'
|
129
133
|
|
134
|
+
## WebAuthn
|
135
|
+
|
136
|
+
ActionAuth's approach for WebAuthn is simplicity. It is used as a multifactor authentication step,
|
137
|
+
so users will still need to register their email address and password. Once the user is registered,
|
138
|
+
they can add a Passkey to their account. The Passkey could be an iCloud Keychain, a hardware security
|
139
|
+
key like a Yubikey, or a mobile device. If enabled and configured, the user will be prompted to use
|
140
|
+
their Passkey after they log in.
|
141
|
+
|
142
|
+
### Configuration
|
143
|
+
|
144
|
+
The migrations are already copied over to your application when you run
|
145
|
+
`bin/rails action_auth:install:migrations`. There are only two steps that you have to take to enable
|
146
|
+
WebAuthn for your application.
|
147
|
+
|
148
|
+
The reason why you need to add the gem is because it's not added to the gemspec of ActionAuth. This is
|
149
|
+
intentional as not all users will want to add this functionality. This will help minimize
|
150
|
+
the number of gems that your application relies on unless if they are features that you want to use.
|
151
|
+
|
152
|
+
#### Add the gem
|
153
|
+
|
154
|
+
```
|
155
|
+
bundle add webauthn
|
156
|
+
```
|
157
|
+
|
158
|
+
### Configure the WebAuthn settings
|
159
|
+
|
160
|
+
**Note:** that the origin name does not have a trailing / or a port number.
|
161
|
+
|
162
|
+
```
|
163
|
+
ActionAuth.configure do |config|
|
164
|
+
config.webauthn_enabled = true
|
165
|
+
config.webauthn_origin = "http://localhost:3000" # or "https://example.com"
|
166
|
+
config.webauthn_rp_name = Rails.application.class.to_s.deconstantize
|
167
|
+
end
|
168
|
+
```
|
130
169
|
|
131
170
|
## License
|
132
171
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -134,5 +173,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
134
173
|
|
135
174
|
## Credits
|
136
175
|
|
137
|
-
Heavily inspired by [Drifting Ruby #300](https://www.driftingruby.com/episodes/authentication-from-scratch)
|
138
|
-
and [Authentication Zero](https://github.com/lazaronixon/authentication-zero)
|
176
|
+
❤️ Heavily inspired by [Drifting Ruby #300](https://www.driftingruby.com/episodes/authentication-from-scratch)
|
177
|
+
and [Authentication Zero](https://github.com/lazaronixon/authentication-zero) and WebAuthn work from
|
178
|
+
[cedarcode](https://www.cedarcode.com/).
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
2
|
+
window.Stimulus = Application.start();
|
3
|
+
|
4
|
+
import * as WebAuthnJSON from 'https://unpkg.com/@github/webauthn-json@2.1.1/dist/esm/webauthn-json.js';
|
5
|
+
const Credential = {
|
6
|
+
getCRFSToken: function () {
|
7
|
+
const CSRFSelector = document.querySelector('meta[name="csrf-token"]');
|
8
|
+
if (CSRFSelector) {
|
9
|
+
return CSRFSelector.getAttribute("content");
|
10
|
+
} else {
|
11
|
+
return null;
|
12
|
+
}
|
13
|
+
},
|
14
|
+
|
15
|
+
callback: function (url, body) {
|
16
|
+
const token = this.getCRFSToken();
|
17
|
+
fetch(url, {
|
18
|
+
method: "POST",
|
19
|
+
body: JSON.stringify(body),
|
20
|
+
headers: {
|
21
|
+
"Content-Type": "application/json",
|
22
|
+
"Accept": "application/json",
|
23
|
+
"X-CSRF-Token": token
|
24
|
+
},
|
25
|
+
credentials: 'same-origin'
|
26
|
+
}).then(function (response) {
|
27
|
+
if (response.ok) {
|
28
|
+
window.location.replace("/");
|
29
|
+
} else if (response.status < 500) {
|
30
|
+
response.text();
|
31
|
+
}
|
32
|
+
});
|
33
|
+
},
|
34
|
+
|
35
|
+
create: function (callbackUrl, credentialOptions) {
|
36
|
+
const self = this;
|
37
|
+
WebAuthnJSON.create({ "publicKey": credentialOptions }).then(function (credential) {
|
38
|
+
self.callback(callbackUrl, credential);
|
39
|
+
});
|
40
|
+
},
|
41
|
+
|
42
|
+
get: function (credentialOptions) {
|
43
|
+
const self = this;
|
44
|
+
const webauthnUrl = document.querySelector('meta[name="webauthn_auth_url"]').getAttribute("content");
|
45
|
+
WebAuthnJSON.get({ "publicKey": credentialOptions }).then(function (credential) {
|
46
|
+
self.callback(webauthnUrl, credential);
|
47
|
+
});
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
|
52
|
+
Stimulus.register(
|
53
|
+
"add-credential",
|
54
|
+
class extends Controller {
|
55
|
+
create(event) {
|
56
|
+
const webauthnUrl = document.querySelector('meta[name="webauthn_cred_url"]').getAttribute("content");
|
57
|
+
const credentialOptions = event.detail;
|
58
|
+
const credential_nickname = event.target.querySelector("input[name='webauthn_credential[nickname]']").value;
|
59
|
+
const callback_url = `${webauthnUrl}/?credential_nickname=${credential_nickname}`
|
60
|
+
Credential.create(encodeURI(callback_url), credentialOptions);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
);
|
64
|
+
|
65
|
+
Stimulus.register(
|
66
|
+
"credential-authenticator",
|
67
|
+
class extends Controller {
|
68
|
+
static values = { options: Object }
|
69
|
+
connect() {
|
70
|
+
console.log(this.optionsValue);
|
71
|
+
if (this.hasOptionsValue) {
|
72
|
+
Credential.get(this.optionsValue);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
);
|
77
|
+
|
78
|
+
document.addEventListener('DOMContentLoaded', function () {
|
79
|
+
const form = document.getElementById('webauthn_credential_form');
|
80
|
+
if (form) {
|
81
|
+
form.addEventListener('submit', function (event) {
|
82
|
+
event.preventDefault();
|
83
|
+
const formData = new FormData(form);
|
84
|
+
fetch(form.action, {
|
85
|
+
method: 'POST',
|
86
|
+
body: formData,
|
87
|
+
headers: {
|
88
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
89
|
+
},
|
90
|
+
credentials: 'same-origin'
|
91
|
+
}).then(response => {
|
92
|
+
return response.json();
|
93
|
+
}).then(data => {
|
94
|
+
form.dispatchEvent(new CustomEvent('ajax:success', { detail: data }));
|
95
|
+
});
|
96
|
+
});
|
97
|
+
}
|
98
|
+
});
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module ActionAuth
|
2
2
|
class SessionsController < ApplicationController
|
3
3
|
before_action :set_current_request_details
|
4
|
+
before_action :authenticate_user!, only: [:index, :destroy]
|
4
5
|
layout "action_auth/application-full-width", only: :index
|
6
|
+
|
5
7
|
def index
|
6
8
|
@sessions = Current.user.action_auth_sessions.order(created_at: :desc)
|
7
9
|
end
|
@@ -11,9 +13,14 @@ module ActionAuth
|
|
11
13
|
|
12
14
|
def create
|
13
15
|
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
if user.second_factor_enabled?
|
17
|
+
session[:webauthn_user_id] = user.id
|
18
|
+
redirect_to new_webauthn_credential_authentications_path
|
19
|
+
else
|
20
|
+
@session = user.action_auth_sessions.create
|
21
|
+
cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
|
22
|
+
redirect_to main_app.root_path, notice: "Signed in successfully"
|
23
|
+
end
|
17
24
|
else
|
18
25
|
redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect"
|
19
26
|
end
|
@@ -22,7 +29,7 @@ module ActionAuth
|
|
22
29
|
def destroy
|
23
30
|
session = Current.user.action_auth_sessions.find(params[:id])
|
24
31
|
session.destroy
|
25
|
-
redirect_to
|
32
|
+
redirect_to main_app.root_path, notice: "That session has been logged out"
|
26
33
|
end
|
27
34
|
end
|
28
35
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class ActionAuth::WebauthnCredentialAuthenticationsController < ApplicationController
|
2
|
+
before_action :ensure_user_not_authenticated
|
3
|
+
before_action :ensure_login_initiated
|
4
|
+
layout "action_auth/application-full-width"
|
5
|
+
|
6
|
+
def new
|
7
|
+
get_options = WebAuthn::Credential.options_for_get(allow: user.action_auth_webauthn_credentials.pluck(:external_id))
|
8
|
+
session[:current_challenge] = get_options.challenge
|
9
|
+
@options = get_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
webauthn_credential = WebAuthn::Credential.from_get(params)
|
14
|
+
|
15
|
+
credential = user.action_auth_webauthn_credentials.find_by(external_id: webauthn_credential.id)
|
16
|
+
|
17
|
+
begin
|
18
|
+
webauthn_credential.verify(
|
19
|
+
session[:current_challenge],
|
20
|
+
public_key: credential.public_key,
|
21
|
+
sign_count: credential.sign_count
|
22
|
+
)
|
23
|
+
|
24
|
+
credential.update!(sign_count: webauthn_credential.sign_count)
|
25
|
+
session.delete(:webauthn_user_id)
|
26
|
+
session = user.action_auth_sessions.create
|
27
|
+
cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
|
28
|
+
render json: { status: "ok" }, status: :ok
|
29
|
+
rescue WebAuthn::Error => e
|
30
|
+
Rails.logger.error "❌ Verification failed: #{e.message}"
|
31
|
+
render json: "Verification failed: #{e.message}", status: :unprocessable_entity
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def user
|
38
|
+
@user ||= ActionAuth::User.find_by(id: session[:webauthn_user_id])
|
39
|
+
end
|
40
|
+
|
41
|
+
def ensure_login_initiated
|
42
|
+
return unless session[:webauthn_user_id].blank?
|
43
|
+
redirect_to sign_in_path
|
44
|
+
end
|
45
|
+
|
46
|
+
def ensure_user_not_authenticated
|
47
|
+
return unless current_user
|
48
|
+
redirect_to main_app.root_path
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class ActionAuth::WebauthnCredentialsController < ApplicationController
|
2
|
+
before_action :authenticate_user!
|
3
|
+
layout "action_auth/application-full-width"
|
4
|
+
|
5
|
+
def new
|
6
|
+
end
|
7
|
+
|
8
|
+
def options
|
9
|
+
if current_user.webauthn_id.blank?
|
10
|
+
current_user.update!(webauthn_id: WebAuthn.generate_user_id)
|
11
|
+
end
|
12
|
+
|
13
|
+
create_options = WebAuthn::Credential.options_for_create(
|
14
|
+
user: {
|
15
|
+
id: current_user.webauthn_id,
|
16
|
+
name: current_user.email
|
17
|
+
},
|
18
|
+
exclude: current_user.action_auth_webauthn_credentials.pluck(:external_id)
|
19
|
+
)
|
20
|
+
|
21
|
+
session[:current_challenge] = create_options.challenge
|
22
|
+
|
23
|
+
respond_to do |format|
|
24
|
+
format.json { render json: create_options }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def create
|
29
|
+
webauthn_credential = WebAuthn::Credential.from_create(params)
|
30
|
+
|
31
|
+
begin
|
32
|
+
webauthn_credential.verify(session[:current_challenge])
|
33
|
+
|
34
|
+
credential = current_user.action_auth_webauthn_credentials.build(
|
35
|
+
external_id: webauthn_credential.id,
|
36
|
+
nickname: params[:credential_nickname],
|
37
|
+
public_key: webauthn_credential.public_key,
|
38
|
+
sign_count: webauthn_credential.sign_count
|
39
|
+
)
|
40
|
+
|
41
|
+
if credential.save
|
42
|
+
render json: { status: "ok" }, status: :ok
|
43
|
+
else
|
44
|
+
render json: "Couldn't add your Security Key", status: :unprocessable_entity
|
45
|
+
end
|
46
|
+
rescue WebAuthn::Error => e
|
47
|
+
Rails.logger.error "❌ Verification failed: #{e.message}"
|
48
|
+
render json: "Verification failed: #{e.message}", status: :unprocessable_entity
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def destroy
|
53
|
+
current_user.action_auth_webauthn_credentials.destroy(params[:id])
|
54
|
+
|
55
|
+
redirect_to sessions_path
|
56
|
+
end
|
57
|
+
end
|
@@ -2,6 +2,14 @@ module ActionAuth
|
|
2
2
|
class User < ApplicationRecord
|
3
3
|
has_secure_password
|
4
4
|
|
5
|
+
has_many :action_auth_sessions, dependent: :destroy,
|
6
|
+
class_name: "ActionAuth::Session", foreign_key: "action_auth_user_id"
|
7
|
+
|
8
|
+
if ActionAuth.configuration.webauthn_enabled?
|
9
|
+
has_many :action_auth_webauthn_credentials, dependent: :destroy,
|
10
|
+
class_name: "ActionAuth::WebauthnCredential", foreign_key: "action_auth_user_id"
|
11
|
+
end
|
12
|
+
|
5
13
|
generates_token_for :email_verification, expires_in: 2.days do
|
6
14
|
email
|
7
15
|
end
|
@@ -10,8 +18,6 @@ module ActionAuth
|
|
10
18
|
password_salt.last(10)
|
11
19
|
end
|
12
20
|
|
13
|
-
has_many :action_auth_sessions, dependent: :destroy, class_name: "ActionAuth::Session", foreign_key: "action_auth_user_id"
|
14
|
-
|
15
21
|
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
16
22
|
validates :password, allow_nil: true, length: { minimum: 12 }
|
17
23
|
|
@@ -24,5 +30,10 @@ module ActionAuth
|
|
24
30
|
after_update if: :password_digest_previously_changed? do
|
25
31
|
action_auth_sessions.where.not(id: Current.session).delete_all
|
26
32
|
end
|
33
|
+
|
34
|
+
def second_factor_enabled?
|
35
|
+
return false unless ActionAuth.configuration.webauthn_enabled?
|
36
|
+
action_auth_webauthn_credentials.any?
|
37
|
+
end
|
27
38
|
end
|
28
39
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActionAuth
|
2
|
+
class WebauthnCredential < ApplicationRecord
|
3
|
+
validates :external_id, :public_key, :nickname, :sign_count, presence: true
|
4
|
+
validates :external_id, uniqueness: true
|
5
|
+
validates :sign_count,
|
6
|
+
numericality: {
|
7
|
+
only_integer: true,
|
8
|
+
greater_than_or_equal_to: 0,
|
9
|
+
less_than_or_equal_to: 2**32 - 1
|
10
|
+
}
|
11
|
+
end
|
12
|
+
end
|
@@ -5,6 +5,7 @@
|
|
5
5
|
</h1>
|
6
6
|
<small><%= link_to "back to app", main_app.root_path %></small>
|
7
7
|
|
8
|
+
<h2>Sessions</h2>
|
8
9
|
<div id="sessions">
|
9
10
|
<table class="action-auth--table">
|
10
11
|
<thead>
|
@@ -28,3 +29,38 @@
|
|
28
29
|
</table>
|
29
30
|
</div>
|
30
31
|
|
32
|
+
<% if ActionAuth.configuration.webauthn_enabled? %>
|
33
|
+
<% if current_user.second_factor_enabled? %>
|
34
|
+
<h3>Your Security Keys:</h3>
|
35
|
+
<table class="action-auth--table">
|
36
|
+
<thead>
|
37
|
+
<tr>
|
38
|
+
<th>Key</th>
|
39
|
+
<th nowrap>Registered On</th>
|
40
|
+
<th nowrap></th>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% current_user.action_auth_webauthn_credentials.each do |credential| %>
|
45
|
+
<%= content_tag :tr, id: dom_id(credential) do %>
|
46
|
+
<td><%= credential.nickname %></td>
|
47
|
+
<td nowrap><%= credential.created_at.strftime('%B %d, %Y') %></td>
|
48
|
+
<td nowrap><%= button_to "Delete", credential, method: :delete, class: "btn btn-primary" %></td>
|
49
|
+
<% end %>
|
50
|
+
<% end %>
|
51
|
+
</tbody>
|
52
|
+
</table>
|
53
|
+
|
54
|
+
<div>
|
55
|
+
<%= button_to("Register new Security Key",
|
56
|
+
new_webauthn_credential_path,
|
57
|
+
method: :get,
|
58
|
+
class: "btn btn-primary") %>
|
59
|
+
</div>
|
60
|
+
<% else %>
|
61
|
+
<h2>Enable Two-Factor Authentication</h2>
|
62
|
+
<div class="">
|
63
|
+
<%= link_to "Add a passkey", new_webauthn_credential_path, class: "btn btn-primary" %>
|
64
|
+
</div>
|
65
|
+
<% end %>
|
66
|
+
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<h2 class="">Use one of your security keys to sign in</h2>
|
2
|
+
|
3
|
+
<%= content_tag :div,
|
4
|
+
id: "webauthn_credential_form",
|
5
|
+
data: { controller: "credential-authenticator",
|
6
|
+
"credential-authenticator-options-value": @options } do %>
|
7
|
+
|
8
|
+
<div class="mb-3">
|
9
|
+
If it's an USB key be sure to insert it and, if necessary, tap it.
|
10
|
+
</div>
|
11
|
+
<% end %>
|
12
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<h2 class="">Add a security key:</h2>
|
2
|
+
<%= form_with scope: :webauthn_credential,
|
3
|
+
url: options_for_webauthn_credentials_path,
|
4
|
+
id: "webauthn_credential_form",
|
5
|
+
data: {
|
6
|
+
controller: "add-credential",
|
7
|
+
action: "ajax:success->add-credential#create",
|
8
|
+
} do |form| %>
|
9
|
+
|
10
|
+
<div class="mb-3">
|
11
|
+
<%= form.text_field :nickname, autofocus: true, placeholder: "New Security Key nickname", required: true %>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="mb-3">
|
15
|
+
<%= form.submit "Add Security Key", class: "btn btn-primary" %>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="">
|
19
|
+
<span class="">If it's an USB key be sure to insert it and, if necessary, tap it.</span>
|
20
|
+
</div>
|
21
|
+
<% end %>
|
@@ -5,6 +5,11 @@
|
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
<%= stylesheet_link_tag "action_auth/application", media: "all" %>
|
8
|
+
<%= javascript_include_tag "action_auth/application", "data-turbo-track": "reload", type: "module" %>
|
9
|
+
<% if ActionAuth.configuration.webauthn_enabled? %>
|
10
|
+
<%= tag :meta, name: :webauthn_auth_url, content: action_auth.webauthn_credential_authentications_url %>
|
11
|
+
<%= tag :meta, name: :webauthn_cred_url, content: action_auth.webauthn_credentials_url %>
|
12
|
+
<% end %>
|
8
13
|
</head>
|
9
14
|
<body class="bg-light">
|
10
15
|
<div class="container-fluid bg-white border pb-3">
|
@@ -5,6 +5,7 @@
|
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
<%= stylesheet_link_tag "action_auth/application", media: "all" %>
|
8
|
+
<%= javascript_include_tag "action_auth/application", "data-turbo-track": "reload", type: "module" %>
|
8
9
|
</head>
|
9
10
|
<body class="bg-light">
|
10
11
|
<div class="container bg-white border pb-3">
|
data/config/routes.rb
CHANGED
@@ -10,4 +10,12 @@ ActionAuth::Engine.routes.draw do
|
|
10
10
|
resource :email_verification, only: [:show, :create]
|
11
11
|
resource :password_reset, only: [:new, :edit, :create, :update]
|
12
12
|
end
|
13
|
+
|
14
|
+
if ActionAuth.configuration.webauthn_enabled?
|
15
|
+
resources :webauthn_credentials, only: [:new, :create, :destroy] do
|
16
|
+
post :options, on: :collection, as: 'options_for'
|
17
|
+
end
|
18
|
+
|
19
|
+
resource :webauthn_credential_authentications, only: [:new, :create]
|
20
|
+
end
|
13
21
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class AddWebauthnCredentials < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :action_auth_webauthn_credentials do |t|
|
4
|
+
t.string :external_id, null: false
|
5
|
+
t.string :public_key, null: false
|
6
|
+
t.string :nickname, null: false
|
7
|
+
t.bigint :sign_count, null: false, default: 0
|
8
|
+
|
9
|
+
t.index :external_id, unique: true
|
10
|
+
|
11
|
+
t.references :action_auth_user, foreign_key: true
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActionAuth
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :webauthn_enabled
|
5
|
+
attr_accessor :webauthn_origin
|
6
|
+
attr_accessor :webauthn_rp_name
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@webauthn_enabled = defined?(WebAuthn)
|
10
|
+
@webauthn_origin = "http://localhost:3000"
|
11
|
+
@webauthn_rp_name = Rails.application.class.to_s.deconstantize
|
12
|
+
end
|
13
|
+
|
14
|
+
def webauthn_enabled?
|
15
|
+
@webauthn_enabled.respond_to?(:call) ? @webauthn_enabled.call : @webauthn_enabled
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -14,6 +14,12 @@ module ActionAuth
|
|
14
14
|
|
15
15
|
def user_signed_in?; Current.user.present?; end
|
16
16
|
helper_method :user_signed_in?
|
17
|
+
|
18
|
+
def authenticate_user!
|
19
|
+
return if user_signed_in?
|
20
|
+
redirect_to new_user_session_path
|
21
|
+
end
|
22
|
+
helper_method :authenticate_user!
|
17
23
|
end
|
18
24
|
|
19
25
|
private
|
data/lib/action_auth/version.rb
CHANGED
data/lib/action_auth.rb
CHANGED
@@ -1,7 +1,23 @@
|
|
1
1
|
require "action_auth/version"
|
2
2
|
require "action_auth/engine"
|
3
|
+
require "action_auth/configuration"
|
3
4
|
|
4
5
|
module ActionAuth
|
5
|
-
|
6
|
+
class << self
|
7
|
+
attr_accessor :configuration
|
8
|
+
|
9
|
+
def configure
|
10
|
+
self.configuration ||= Configuration.new
|
11
|
+
yield(configuration)
|
12
|
+
configure_webauthn
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure_webauthn
|
16
|
+
return unless configuration.webauthn_enabled?
|
17
|
+
WebAuthn.configure do |config|
|
18
|
+
config.origin = configuration.webauthn_origin
|
19
|
+
config.rp_name = configuration.webauthn_rp_name
|
20
|
+
end
|
21
|
+
end
|
6
22
|
end
|
7
23
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dave Kimura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-01-
|
11
|
+
date: 2024-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -50,6 +50,7 @@ files:
|
|
50
50
|
- README.md
|
51
51
|
- Rakefile
|
52
52
|
- app/assets/config/action_auth_manifest.js
|
53
|
+
- app/assets/javascripts/action_auth/application.js
|
53
54
|
- app/assets/stylesheets/action_auth/application.css
|
54
55
|
- app/controllers/action_auth/application_controller.rb
|
55
56
|
- app/controllers/action_auth/identity/email_verifications_controller.rb
|
@@ -58,6 +59,8 @@ files:
|
|
58
59
|
- app/controllers/action_auth/passwords_controller.rb
|
59
60
|
- app/controllers/action_auth/registrations_controller.rb
|
60
61
|
- app/controllers/action_auth/sessions_controller.rb
|
62
|
+
- app/controllers/action_auth/webauthn_credential_authentications_controller.rb
|
63
|
+
- app/controllers/action_auth/webauthn_credentials_controller.rb
|
61
64
|
- app/helpers/action_auth/application_helper.rb
|
62
65
|
- app/jobs/action_auth/application_job.rb
|
63
66
|
- app/mailers/action_auth/application_mailer.rb
|
@@ -66,6 +69,7 @@ files:
|
|
66
69
|
- app/models/action_auth/current.rb
|
67
70
|
- app/models/action_auth/session.rb
|
68
71
|
- app/models/action_auth/user.rb
|
72
|
+
- app/models/action_auth/webauthn_credential.rb
|
69
73
|
- app/views/action_auth/identity/emails/edit.html.erb
|
70
74
|
- app/views/action_auth/identity/password_resets/edit.html.erb
|
71
75
|
- app/views/action_auth/identity/password_resets/new.html.erb
|
@@ -77,6 +81,8 @@ files:
|
|
77
81
|
- app/views/action_auth/user_mailer/email_verification.text.erb
|
78
82
|
- app/views/action_auth/user_mailer/password_reset.html.erb
|
79
83
|
- app/views/action_auth/user_mailer/password_reset.text.erb
|
84
|
+
- app/views/action_auth/webauthn_credential_authentications/new.html.erb
|
85
|
+
- app/views/action_auth/webauthn_credentials/new.html.erb
|
80
86
|
- app/views/layouts/action_auth/application-full-width.html.erb
|
81
87
|
- app/views/layouts/action_auth/application.html.erb
|
82
88
|
- app/views/layouts/action_auth/mailer.html.erb
|
@@ -84,7 +90,10 @@ files:
|
|
84
90
|
- config/routes.rb
|
85
91
|
- db/migrate/20231107165548_create_action_auth_users.rb
|
86
92
|
- db/migrate/20231107170349_create_action_auth_sessions.rb
|
93
|
+
- db/migrate/20240111125859_add_webauthn_credentials.rb
|
94
|
+
- db/migrate/20240111142545_add_webauthn_id_to_users.rb
|
87
95
|
- lib/action_auth.rb
|
96
|
+
- lib/action_auth/configuration.rb
|
88
97
|
- lib/action_auth/controllers/helpers.rb
|
89
98
|
- lib/action_auth/engine.rb
|
90
99
|
- lib/action_auth/routing/helpers.rb
|