action_auth 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +96 -0
- data/app/controllers/action_auth/sessions_controller.rb +11 -4
- data/app/controllers/action_auth/webauthn_credential_authentications_controller.rb +57 -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 +16 -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 +10 -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: 2da21219355f4b3f9fedb9ebd753a39468ca5e5d4f818a704acf67c99ec1f595
|
4
|
+
data.tar.gz: cebdf0acc073b632b4c393b923ef4e76580f52383503ee0702d46b752099fe6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5eb15aaec06bcf5779e690e70acac48046a1fcbd17efc52270a4a542b2913f489c7ff8da42632ffe86f5b9860652de9aae8a76f6a54b4261923bc441d6d6ba42
|
7
|
+
data.tar.gz: 405ae5545bf0d919317d4944f0c85b99dba7e5e7f7ea70d391a516838c75ff1780633da9672cf2da2474ede30e01ae5d9bd883977f18c3fbb9d036b5a0285120
|
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,96 @@
|
|
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
|
+
verifyKey(event) {
|
69
|
+
const credentialOptions = event.detail;
|
70
|
+
console.log(credentialOptions);
|
71
|
+
Credential.get(credentialOptions);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
);
|
75
|
+
|
76
|
+
document.addEventListener('DOMContentLoaded', function () {
|
77
|
+
const form = document.getElementById('webauthn_credential_form');
|
78
|
+
if (form) {
|
79
|
+
form.addEventListener('submit', function (event) {
|
80
|
+
event.preventDefault();
|
81
|
+
const formData = new FormData(form);
|
82
|
+
fetch(form.action, {
|
83
|
+
method: 'POST',
|
84
|
+
body: formData,
|
85
|
+
headers: {
|
86
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
87
|
+
},
|
88
|
+
credentials: 'same-origin'
|
89
|
+
}).then(response => {
|
90
|
+
return response.json();
|
91
|
+
}).then(data => {
|
92
|
+
form.dispatchEvent(new CustomEvent('ajax:success', { detail: data }));
|
93
|
+
});
|
94
|
+
});
|
95
|
+
}
|
96
|
+
});
|
@@ -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,57 @@
|
|
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
|
+
end
|
8
|
+
|
9
|
+
def options
|
10
|
+
get_options = WebAuthn::Credential.options_for_get(allow: user.action_auth_webauthn_credentials.pluck(:external_id))
|
11
|
+
|
12
|
+
session[:current_challenge] = get_options.challenge
|
13
|
+
|
14
|
+
respond_to do |format|
|
15
|
+
format.json { render json: get_options }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create
|
20
|
+
webauthn_credential = WebAuthn::Credential.from_get(params)
|
21
|
+
|
22
|
+
credential = user.action_auth_webauthn_credentials.find_by(external_id: webauthn_credential.id)
|
23
|
+
|
24
|
+
begin
|
25
|
+
webauthn_credential.verify(
|
26
|
+
session[:current_challenge],
|
27
|
+
public_key: credential.public_key,
|
28
|
+
sign_count: credential.sign_count
|
29
|
+
)
|
30
|
+
|
31
|
+
credential.update!(sign_count: webauthn_credential.sign_count)
|
32
|
+
session.delete(:webauthn_user_id)
|
33
|
+
session = user.action_auth_sessions.create
|
34
|
+
cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
|
35
|
+
render json: { status: "ok" }, status: :ok
|
36
|
+
rescue WebAuthn::Error => e
|
37
|
+
Rails.logger.error "❌ Verification failed: #{e.message}"
|
38
|
+
render json: "Verification failed: #{e.message}", status: :unprocessable_entity
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def user
|
45
|
+
@user ||= ActionAuth::User.find_by(id: session[:webauthn_user_id])
|
46
|
+
end
|
47
|
+
|
48
|
+
def ensure_login_initiated
|
49
|
+
return unless session[:webauthn_user_id].blank?
|
50
|
+
redirect_to sign_in_path
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure_user_not_authenticated
|
54
|
+
return unless current_user
|
55
|
+
redirect_to main_app.root_path
|
56
|
+
end
|
57
|
+
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,16 @@
|
|
1
|
+
<h2 class="">Use one of your security keys to sign in</h2>
|
2
|
+
|
3
|
+
<%= form_with scope: :webauthn_credential_authentication,
|
4
|
+
url: options_for_webauthn_credential_authentications_path,
|
5
|
+
id: "webauthn_credential_form",
|
6
|
+
data: { controller: "credential-authenticator", action: "ajax:success->credential-authenticator#verifyKey" } do |form| %>
|
7
|
+
|
8
|
+
<div class="mb-3">
|
9
|
+
<%= form.submit "Use Security Key", class: "btn btn-primary" %>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="mb-3">
|
13
|
+
If it's an USB key be sure to insert it and, if necessary, tap it.
|
14
|
+
</div>
|
15
|
+
<% end %>
|
16
|
+
|
@@ -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,14 @@ 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] do
|
20
|
+
post :options, on: :collection, as: 'options_for'
|
21
|
+
end
|
22
|
+
end
|
13
23
|
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.
|
4
|
+
version: 0.2.0
|
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
|