has_secure_passkey 0.2.7 → 0.3.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 +4 -4
- data/app/assets/javascripts/components/web_authn.js +12 -4
- data/app/assets/javascripts/has_secure_passkey.js +168 -195
- data/lib/has_secure_passkey/active_record_helpers.rb +12 -0
- data/lib/has_secure_passkey/options_for_create.rb +1 -1
- data/lib/has_secure_passkey/options_for_get.rb +1 -1
- data/lib/has_secure_passkey/recovery.rb +35 -0
- data/lib/has_secure_passkey/version.rb +1 -1
- data/lib/has_secure_passkey.rb +5 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8701c9e46b1dfce56e62338235a894088e6973b2ae425ba531cd683c3e1d436
|
4
|
+
data.tar.gz: 8b5312f7fecd005bdf6cd058a5a5d03f8c884424d0d759ad4e8b444e73a236bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47ea5e397d57f8bf7b6646674f6a3fee6cdac26302de8d9f02da0e721675a50110af46c35e50cd2bade07b549fc7282d9e64253967b3bb2c3528af88857d69af
|
7
|
+
data.tar.gz: b5bae0850c9e4238798ce232ab0ff0bb746b96c8466c7571e748ca84e9cb045a54d2b1754bc7e3253208dcfd0f25820436815de85553fef9d8fcecc5472ac22e
|
@@ -1,5 +1,4 @@
|
|
1
|
-
import
|
2
|
-
import { post } from "@rails/request"
|
1
|
+
import { post } from "@rails/request.js"
|
3
2
|
|
4
3
|
export default class WebAuthn extends HTMLElement {
|
5
4
|
static observedAttributes = ["action", "callback", "options"];
|
@@ -16,13 +15,13 @@ export default class WebAuthn extends HTMLElement {
|
|
16
15
|
}
|
17
16
|
|
18
17
|
run() {
|
19
|
-
|
18
|
+
navigator.credentials[this.action]({publicKey: this.publicKey}).
|
20
19
|
then(async (credential) => {
|
21
20
|
this.showProgress()
|
22
21
|
|
23
22
|
const { response, redirected } = await post(this.callback, {
|
24
23
|
responseKind: "turbo-stream",
|
25
|
-
body: JSON.stringify(Object.assign(credential, { webauthn_message: this.message }))
|
24
|
+
body: JSON.stringify(Object.assign(credential.toJSON(), { webauthn_message: this.message }))
|
26
25
|
})
|
27
26
|
|
28
27
|
this.hideProgress()
|
@@ -34,6 +33,7 @@ export default class WebAuthn extends HTMLElement {
|
|
34
33
|
Turbo.navigator.proposeVisit(new URL(response.url), options)
|
35
34
|
}
|
36
35
|
}).catch((error) => {
|
36
|
+
console.error(error)
|
37
37
|
this.onError(error)
|
38
38
|
})
|
39
39
|
}
|
@@ -77,4 +77,12 @@ export default class WebAuthn extends HTMLElement {
|
|
77
77
|
get message() {
|
78
78
|
return this.getAttribute('message');
|
79
79
|
}
|
80
|
+
|
81
|
+
get publicKey() {
|
82
|
+
if (this.action === "create") {
|
83
|
+
return PublicKeyCredential.parseCreationOptionsFromJSON(this.options);
|
84
|
+
} else {
|
85
|
+
return PublicKeyCredential.parseRequestOptionsFromJSON(this.options);
|
86
|
+
}
|
87
|
+
}
|
80
88
|
}
|
@@ -1,113 +1,7 @@
|
|
1
|
-
|
2
|
-
var __export = (target, all) => {
|
3
|
-
for (var name in all)
|
4
|
-
__defProp(target, name, {
|
5
|
-
get: all[name],
|
6
|
-
enumerable: true,
|
7
|
-
configurable: true,
|
8
|
-
set: (newValue) => all[name] = () => newValue
|
9
|
-
});
|
10
|
-
};
|
11
|
-
|
12
|
-
// app/assets/javascripts/@github--webauthn-json.js
|
13
|
-
var exports__github_webauthn_json = {};
|
14
|
-
__export(exports__github_webauthn_json, {
|
15
|
-
supported: () => supported,
|
16
|
-
schema: () => c,
|
17
|
-
get: () => get,
|
18
|
-
create: () => create
|
19
|
-
});
|
20
|
-
function base64urlToBuffer(e) {
|
21
|
-
const r = "==".slice(0, (4 - e.length % 4) % 4);
|
22
|
-
const t = e.replace(/-/g, "+").replace(/_/g, "/") + r;
|
23
|
-
const n = atob(t);
|
24
|
-
const i = new ArrayBuffer(n.length);
|
25
|
-
const o = new Uint8Array(i);
|
26
|
-
for (let e2 = 0;e2 < n.length; e2++)
|
27
|
-
o[e2] = n.charCodeAt(e2);
|
28
|
-
return i;
|
29
|
-
}
|
30
|
-
function bufferToBase64url(e) {
|
31
|
-
const r = new Uint8Array(e);
|
32
|
-
let t = "";
|
33
|
-
for (const e2 of r)
|
34
|
-
t += String.fromCharCode(e2);
|
35
|
-
const n = btoa(t);
|
36
|
-
const i = n.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
37
|
-
return i;
|
38
|
-
}
|
39
|
-
var e = "copy";
|
40
|
-
var r = "convert";
|
41
|
-
function convert(t, n, i) {
|
42
|
-
if (n === e)
|
43
|
-
return i;
|
44
|
-
if (n === r)
|
45
|
-
return t(i);
|
46
|
-
if (n instanceof Array)
|
47
|
-
return i.map((e2) => convert(t, n[0], e2));
|
48
|
-
if (n instanceof Object) {
|
49
|
-
const e2 = {};
|
50
|
-
for (const [r2, o] of Object.entries(n)) {
|
51
|
-
if (o.derive) {
|
52
|
-
const e3 = o.derive(i);
|
53
|
-
e3 !== undefined && (i[r2] = e3);
|
54
|
-
}
|
55
|
-
if (r2 in i)
|
56
|
-
i[r2] != null ? e2[r2] = convert(t, o.schema, i[r2]) : e2[r2] = null;
|
57
|
-
else if (o.required)
|
58
|
-
throw new Error(`Missing key: ${r2}`);
|
59
|
-
}
|
60
|
-
return e2;
|
61
|
-
}
|
62
|
-
}
|
63
|
-
function derived(e2, r2) {
|
64
|
-
return { required: true, schema: e2, derive: r2 };
|
65
|
-
}
|
66
|
-
function required(e2) {
|
67
|
-
return { required: true, schema: e2 };
|
68
|
-
}
|
69
|
-
function optional(e2) {
|
70
|
-
return { required: false, schema: e2 };
|
71
|
-
}
|
72
|
-
var t = { type: required(e), id: required(r), transports: optional(e) };
|
73
|
-
var n = { appid: optional(e), appidExclude: optional(e), credProps: optional(e) };
|
74
|
-
var i = { appid: optional(e), appidExclude: optional(e), credProps: optional(e) };
|
75
|
-
var o = { publicKey: required({ rp: required(e), user: required({ id: required(r), name: required(e), displayName: required(e) }), challenge: required(r), pubKeyCredParams: required(e), timeout: optional(e), excludeCredentials: optional([t]), authenticatorSelection: optional(e), attestation: optional(e), extensions: optional(n) }), signal: optional(e) };
|
76
|
-
var a = { type: required(e), id: required(e), rawId: required(r), authenticatorAttachment: optional(e), response: required({ clientDataJSON: required(r), attestationObject: required(r), transports: derived(e, (e2) => {
|
77
|
-
var r2;
|
78
|
-
return ((r2 = e2.getTransports) == null ? undefined : r2.call(e2)) || [];
|
79
|
-
}) }), clientExtensionResults: derived(i, (e2) => e2.getClientExtensionResults()) };
|
80
|
-
var u = { mediation: optional(e), publicKey: required({ challenge: required(r), timeout: optional(e), rpId: optional(e), allowCredentials: optional([t]), userVerification: optional(e), extensions: optional(n) }), signal: optional(e) };
|
81
|
-
var s = { type: required(e), id: required(e), rawId: required(r), authenticatorAttachment: optional(e), response: required({ clientDataJSON: required(r), authenticatorData: required(r), signature: required(r), userHandle: required(r) }), clientExtensionResults: derived(i, (e2) => e2.getClientExtensionResults()) };
|
82
|
-
var c = { credentialCreationOptions: o, publicKeyCredentialWithAttestation: a, credentialRequestOptions: u, publicKeyCredentialWithAssertion: s };
|
83
|
-
function createRequestFromJSON(e2) {
|
84
|
-
return convert(base64urlToBuffer, o, e2);
|
85
|
-
}
|
86
|
-
function createResponseToJSON(e2) {
|
87
|
-
return convert(bufferToBase64url, a, e2);
|
88
|
-
}
|
89
|
-
async function create(e2) {
|
90
|
-
const r2 = await navigator.credentials.create(createRequestFromJSON(e2));
|
91
|
-
return createResponseToJSON(r2);
|
92
|
-
}
|
93
|
-
function getRequestFromJSON(e2) {
|
94
|
-
return convert(base64urlToBuffer, u, e2);
|
95
|
-
}
|
96
|
-
function getResponseToJSON(e2) {
|
97
|
-
return convert(bufferToBase64url, s, e2);
|
98
|
-
}
|
99
|
-
async function get(e2) {
|
100
|
-
const r2 = await navigator.credentials.get(getRequestFromJSON(e2));
|
101
|
-
return getResponseToJSON(r2);
|
102
|
-
}
|
103
|
-
function supported() {
|
104
|
-
return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
|
105
|
-
}
|
106
|
-
|
107
|
-
// app/assets/javascripts/@rails--request.js
|
1
|
+
// node_modules/@rails/request.js/src/fetch_response.js
|
108
2
|
class FetchResponse {
|
109
|
-
constructor(
|
110
|
-
this.response =
|
3
|
+
constructor(response) {
|
4
|
+
this.response = response;
|
111
5
|
}
|
112
6
|
get statusCode() {
|
113
7
|
return this.response.status;
|
@@ -128,17 +22,23 @@ class FetchResponse {
|
|
128
22
|
return this.response.headers.get("WWW-Authenticate");
|
129
23
|
}
|
130
24
|
get contentType() {
|
131
|
-
const
|
132
|
-
return
|
25
|
+
const contentType = this.response.headers.get("Content-Type") || "";
|
26
|
+
return contentType.replace(/;.*$/, "");
|
133
27
|
}
|
134
28
|
get headers() {
|
135
29
|
return this.response.headers;
|
136
30
|
}
|
137
31
|
get html() {
|
138
|
-
|
32
|
+
if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) {
|
33
|
+
return this.text;
|
34
|
+
}
|
35
|
+
return Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
|
139
36
|
}
|
140
37
|
get json() {
|
141
|
-
|
38
|
+
if (this.contentType.match(/^application\/.*json$/)) {
|
39
|
+
return this.responseJson || (this.responseJson = this.response.json());
|
40
|
+
}
|
41
|
+
return Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
|
142
42
|
}
|
143
43
|
get text() {
|
144
44
|
return this.responseText || (this.responseText = this.response.text());
|
@@ -150,27 +50,38 @@ class FetchResponse {
|
|
150
50
|
return this.contentType.match(/\b(?:java|ecma)script\b/);
|
151
51
|
}
|
152
52
|
async renderTurboStream() {
|
153
|
-
if (
|
53
|
+
if (this.isTurboStream) {
|
54
|
+
if (window.Turbo) {
|
55
|
+
await window.Turbo.renderStreamMessage(await this.text);
|
56
|
+
} else {
|
57
|
+
console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
|
58
|
+
}
|
59
|
+
} else {
|
154
60
|
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
|
155
|
-
|
61
|
+
}
|
156
62
|
}
|
157
63
|
async activeScript() {
|
158
|
-
if (
|
64
|
+
if (this.isScript) {
|
65
|
+
const script = document.createElement("script");
|
66
|
+
const metaTag = document.querySelector("meta[name=csp-nonce]");
|
67
|
+
if (metaTag) {
|
68
|
+
const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
|
69
|
+
if (nonce) {
|
70
|
+
script.setAttribute("nonce", nonce);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
script.innerHTML = await this.text;
|
74
|
+
document.body.appendChild(script);
|
75
|
+
} else {
|
159
76
|
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
|
160
|
-
{
|
161
|
-
const t2 = document.createElement("script");
|
162
|
-
const e2 = document.querySelector("meta[name=csp-nonce]");
|
163
|
-
const n2 = e2 && e2.content;
|
164
|
-
n2 && t2.setAttribute("nonce", n2);
|
165
|
-
t2.innerHTML = await this.text;
|
166
|
-
document.body.appendChild(t2);
|
167
77
|
}
|
168
78
|
}
|
169
79
|
}
|
170
80
|
|
81
|
+
// node_modules/@rails/request.js/src/request_interceptor.js
|
171
82
|
class RequestInterceptor {
|
172
|
-
static register(
|
173
|
-
this.interceptor =
|
83
|
+
static register(interceptor) {
|
84
|
+
this.interceptor = interceptor;
|
174
85
|
}
|
175
86
|
static get() {
|
176
87
|
return this.interceptor;
|
@@ -179,90 +90,130 @@ class RequestInterceptor {
|
|
179
90
|
this.interceptor = undefined;
|
180
91
|
}
|
181
92
|
}
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
const
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
93
|
+
|
94
|
+
// node_modules/@rails/request.js/src/lib/utils.js
|
95
|
+
function getCookie(name) {
|
96
|
+
const cookies = document.cookie ? document.cookie.split("; ") : [];
|
97
|
+
const prefix = `${encodeURIComponent(name)}=`;
|
98
|
+
const cookie = cookies.find((cookie2) => cookie2.startsWith(prefix));
|
99
|
+
if (cookie) {
|
100
|
+
const value = cookie.split("=").slice(1).join("=");
|
101
|
+
if (value) {
|
102
|
+
return decodeURIComponent(value);
|
103
|
+
}
|
190
104
|
}
|
191
105
|
}
|
192
|
-
function compact(
|
193
|
-
const
|
194
|
-
for (const
|
195
|
-
const
|
196
|
-
|
106
|
+
function compact(object) {
|
107
|
+
const result = {};
|
108
|
+
for (const key in object) {
|
109
|
+
const value = object[key];
|
110
|
+
if (value !== undefined) {
|
111
|
+
result[key] = value;
|
112
|
+
}
|
197
113
|
}
|
198
|
-
return
|
114
|
+
return result;
|
199
115
|
}
|
200
|
-
function metaContent(
|
201
|
-
const
|
202
|
-
return
|
116
|
+
function metaContent(name) {
|
117
|
+
const element = document.head.querySelector(`meta[name="${name}"]`);
|
118
|
+
return element && element.content;
|
203
119
|
}
|
204
|
-
function stringEntriesFromFormData(
|
205
|
-
return [...
|
120
|
+
function stringEntriesFromFormData(formData) {
|
121
|
+
return [...formData].reduce((entries, [name, value]) => {
|
122
|
+
return entries.concat(typeof value === "string" ? [[name, value]] : []);
|
123
|
+
}, []);
|
206
124
|
}
|
207
|
-
function mergeEntries(
|
208
|
-
for (const [
|
209
|
-
if (
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
125
|
+
function mergeEntries(searchParams, entries) {
|
126
|
+
for (const [name, value] of entries) {
|
127
|
+
if (value instanceof window.File)
|
128
|
+
continue;
|
129
|
+
if (searchParams.has(name) && !name.includes("[]")) {
|
130
|
+
searchParams.delete(name);
|
131
|
+
searchParams.set(name, value);
|
132
|
+
} else {
|
133
|
+
searchParams.append(name, value);
|
134
|
+
}
|
135
|
+
}
|
215
136
|
}
|
216
137
|
|
138
|
+
// node_modules/@rails/request.js/src/fetch_request.js
|
217
139
|
class FetchRequest {
|
218
|
-
constructor(
|
219
|
-
this.method =
|
220
|
-
this.options =
|
221
|
-
this.originalUrl =
|
140
|
+
constructor(method, url, options = {}) {
|
141
|
+
this.method = method;
|
142
|
+
this.options = options;
|
143
|
+
this.originalUrl = url.toString();
|
222
144
|
}
|
223
145
|
async perform() {
|
224
146
|
try {
|
225
|
-
const
|
226
|
-
|
227
|
-
|
228
|
-
|
147
|
+
const requestInterceptor = RequestInterceptor.get();
|
148
|
+
if (requestInterceptor) {
|
149
|
+
await requestInterceptor(this);
|
150
|
+
}
|
151
|
+
} catch (error) {
|
152
|
+
console.error(error);
|
153
|
+
}
|
154
|
+
const fetch = this.responseKind === "turbo-stream" && window.Turbo ? window.Turbo.fetch : window.fetch;
|
155
|
+
const response = new FetchResponse(await fetch(this.url, this.fetchOptions));
|
156
|
+
if (response.unauthenticated && response.authenticationURL) {
|
157
|
+
return Promise.reject(window.location.href = response.authenticationURL);
|
158
|
+
}
|
159
|
+
if (response.isScript) {
|
160
|
+
await response.activeScript();
|
161
|
+
}
|
162
|
+
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
|
163
|
+
if (responseStatusIsTurboStreamable && response.isTurboStream) {
|
164
|
+
await response.renderTurboStream();
|
229
165
|
}
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
n2 && e2.isTurboStream && await e2.renderTurboStream();
|
237
|
-
return e2;
|
238
|
-
}
|
239
|
-
addHeader(t2, e2) {
|
240
|
-
const n2 = this.additionalHeaders;
|
241
|
-
n2[t2] = e2;
|
242
|
-
this.options.headers = n2;
|
166
|
+
return response;
|
167
|
+
}
|
168
|
+
addHeader(key, value) {
|
169
|
+
const headers = this.additionalHeaders;
|
170
|
+
headers[key] = value;
|
171
|
+
this.options.headers = headers;
|
243
172
|
}
|
244
173
|
sameHostname() {
|
245
|
-
if (!this.originalUrl.startsWith("http:"))
|
174
|
+
if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
|
246
175
|
return true;
|
176
|
+
}
|
247
177
|
try {
|
248
178
|
return new URL(this.originalUrl).hostname === window.location.hostname;
|
249
|
-
} catch (
|
179
|
+
} catch (_) {
|
250
180
|
return true;
|
251
181
|
}
|
252
182
|
}
|
253
183
|
get fetchOptions() {
|
254
|
-
return {
|
184
|
+
return {
|
185
|
+
method: this.method.toUpperCase(),
|
186
|
+
headers: this.headers,
|
187
|
+
body: this.formattedBody,
|
188
|
+
signal: this.signal,
|
189
|
+
credentials: this.credentials,
|
190
|
+
redirect: this.redirect,
|
191
|
+
keepalive: this.keepalive
|
192
|
+
};
|
255
193
|
}
|
256
194
|
get headers() {
|
257
|
-
const
|
258
|
-
|
259
|
-
|
195
|
+
const baseHeaders = {
|
196
|
+
"X-Requested-With": "XMLHttpRequest",
|
197
|
+
"Content-Type": this.contentType,
|
198
|
+
Accept: this.accept
|
199
|
+
};
|
200
|
+
if (this.sameHostname()) {
|
201
|
+
baseHeaders["X-CSRF-Token"] = this.csrfToken;
|
202
|
+
}
|
203
|
+
return compact(Object.assign(baseHeaders, this.additionalHeaders));
|
260
204
|
}
|
261
205
|
get csrfToken() {
|
262
206
|
return getCookie(metaContent("csrf-param")) || metaContent("csrf-token");
|
263
207
|
}
|
264
208
|
get contentType() {
|
265
|
-
|
209
|
+
if (this.options.contentType) {
|
210
|
+
return this.options.contentType;
|
211
|
+
} else if (this.body == null || this.body instanceof window.FormData) {
|
212
|
+
return;
|
213
|
+
} else if (this.body instanceof window.File) {
|
214
|
+
return this.body.type;
|
215
|
+
}
|
216
|
+
return "application/json";
|
266
217
|
}
|
267
218
|
get accept() {
|
268
219
|
switch (this.responseKind) {
|
@@ -282,13 +233,19 @@ class FetchRequest {
|
|
282
233
|
return this.options.body;
|
283
234
|
}
|
284
235
|
get query() {
|
285
|
-
const
|
286
|
-
const
|
287
|
-
let
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
236
|
+
const originalQuery = (this.originalUrl.split("?")[1] || "").split("#")[0];
|
237
|
+
const params = new URLSearchParams(originalQuery);
|
238
|
+
let requestQuery = this.options.query;
|
239
|
+
if (requestQuery instanceof window.FormData) {
|
240
|
+
requestQuery = stringEntriesFromFormData(requestQuery);
|
241
|
+
} else if (requestQuery instanceof window.URLSearchParams) {
|
242
|
+
requestQuery = requestQuery.entries();
|
243
|
+
} else {
|
244
|
+
requestQuery = Object.entries(requestQuery || {});
|
245
|
+
}
|
246
|
+
mergeEntries(params, requestQuery);
|
247
|
+
const query = params.toString();
|
248
|
+
return query.length > 0 ? `?${query}` : "";
|
292
249
|
}
|
293
250
|
get url() {
|
294
251
|
return this.originalUrl.split("?")[0].split("#")[0] + this.query;
|
@@ -305,18 +262,26 @@ class FetchRequest {
|
|
305
262
|
get credentials() {
|
306
263
|
return this.options.credentials || "same-origin";
|
307
264
|
}
|
265
|
+
get keepalive() {
|
266
|
+
return this.options.keepalive || false;
|
267
|
+
}
|
308
268
|
get additionalHeaders() {
|
309
269
|
return this.options.headers || {};
|
310
270
|
}
|
311
271
|
get formattedBody() {
|
312
|
-
const
|
313
|
-
const
|
314
|
-
|
272
|
+
const bodyIsAString = Object.prototype.toString.call(this.body) === "[object String]";
|
273
|
+
const contentTypeIsJson = this.headers["Content-Type"] === "application/json";
|
274
|
+
if (contentTypeIsJson && !bodyIsAString) {
|
275
|
+
return JSON.stringify(this.body);
|
276
|
+
}
|
277
|
+
return this.body;
|
315
278
|
}
|
316
279
|
}
|
317
|
-
|
318
|
-
|
319
|
-
|
280
|
+
|
281
|
+
// node_modules/@rails/request.js/src/verbs.js
|
282
|
+
async function post(url, options) {
|
283
|
+
const request = new FetchRequest("post", url, options);
|
284
|
+
return request.perform();
|
320
285
|
}
|
321
286
|
|
322
287
|
// app/assets/javascripts/components/web_authn.js
|
@@ -332,11 +297,11 @@ class WebAuthn extends HTMLElement {
|
|
332
297
|
this.run();
|
333
298
|
}
|
334
299
|
run() {
|
335
|
-
|
300
|
+
navigator.credentials[this.action]({ publicKey: this.publicKey }).then(async (credential) => {
|
336
301
|
this.showProgress();
|
337
302
|
const { response, redirected } = await post(this.callback, {
|
338
303
|
responseKind: "turbo-stream",
|
339
|
-
body: JSON.stringify(Object.assign(credential, { webauthn_message: this.message }))
|
304
|
+
body: JSON.stringify(Object.assign(credential.toJSON(), { webauthn_message: this.message }))
|
340
305
|
});
|
341
306
|
this.hideProgress();
|
342
307
|
if (response.ok && redirected) {
|
@@ -349,6 +314,7 @@ class WebAuthn extends HTMLElement {
|
|
349
314
|
Turbo.navigator.proposeVisit(new URL(response.url), options);
|
350
315
|
}
|
351
316
|
}).catch((error) => {
|
317
|
+
console.error(error);
|
352
318
|
this.onError(error);
|
353
319
|
});
|
354
320
|
}
|
@@ -383,6 +349,13 @@ class WebAuthn extends HTMLElement {
|
|
383
349
|
get message() {
|
384
350
|
return this.getAttribute("message");
|
385
351
|
}
|
352
|
+
get publicKey() {
|
353
|
+
if (this.action === "create") {
|
354
|
+
return PublicKeyCredential.parseCreationOptionsFromJSON(this.options);
|
355
|
+
} else {
|
356
|
+
return PublicKeyCredential.parseRequestOptionsFromJSON(this.options);
|
357
|
+
}
|
358
|
+
}
|
386
359
|
}
|
387
360
|
|
388
361
|
// app/assets/javascripts/components/has_secure_passkey.js
|
@@ -22,6 +22,10 @@ module HasSecurePasskey::ActiveRecordHelpers
|
|
22
22
|
authenticated
|
23
23
|
end
|
24
24
|
|
25
|
+
define_singleton_method :recover_passkey do |params:|
|
26
|
+
HasSecurePasskey::Recovery.new(model: self, params:).run
|
27
|
+
end
|
28
|
+
|
25
29
|
define_singleton_method :create_by_webauthn do |params:|
|
26
30
|
authenticatable = new(HasSecurePasskey::OptionsForCreate.
|
27
31
|
from_message(params[:webauthn_message]).authenticatable)
|
@@ -41,6 +45,14 @@ module HasSecurePasskey::ActiveRecordHelpers
|
|
41
45
|
save
|
42
46
|
end
|
43
47
|
|
48
|
+
define_method :reset_webauthn_id do
|
49
|
+
self.webauthn_id = self.class.webauthn_id
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method :passkey_recovery_token do
|
53
|
+
to_sgid(expires_in: 1.hour, for: :recovery).to_s
|
54
|
+
end
|
55
|
+
|
44
56
|
define_method :encode_webauthn_message do
|
45
57
|
HasSecurePasskey::OptionsForCreate.new(authenticatable: self).message
|
46
58
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class HasSecurePasskey::Recovery
|
2
|
+
def initialize(model:, params:)
|
3
|
+
@model = model
|
4
|
+
@params = params
|
5
|
+
end
|
6
|
+
|
7
|
+
def run
|
8
|
+
old_passkeys = authenticatable.passkeys.to_a
|
9
|
+
|
10
|
+
ActiveRecord::Base.transaction do
|
11
|
+
(authenticatable.update(webauthn_id:) &&
|
12
|
+
authenticatable.add_passkey(params:) &&
|
13
|
+
old_passkeys.all?(&:destroy) && authenticatable) ||
|
14
|
+
raise(ActiveRecord::Rollback)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :params, :model
|
21
|
+
|
22
|
+
def authenticatable
|
23
|
+
model.find(options.authenticatable[:id])
|
24
|
+
end
|
25
|
+
|
26
|
+
def webauthn_id
|
27
|
+
options.authenticatable[:webauthn_id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def options
|
31
|
+
HasSecurePasskey::OptionsForCreate.from_message(params[:webauthn_message])
|
32
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
33
|
+
""
|
34
|
+
end
|
35
|
+
end
|
data/lib/has_secure_passkey.rb
CHANGED
@@ -9,4 +9,9 @@ require "has_secure_passkey/authenticate_by"
|
|
9
9
|
require "has_secure_passkey/add_passkey"
|
10
10
|
|
11
11
|
module HasSecurePasskey
|
12
|
+
def self.find_recovery_token(token)
|
13
|
+
GlobalID::Locator.
|
14
|
+
locate_signed(token, for: :recovery).
|
15
|
+
tap { it&.reset_webauthn_id } || raise(ActiveRecord::RecordNotFound)
|
16
|
+
end
|
12
17
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_secure_passkey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Pezza
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- lib/has_secure_passkey/engine.rb
|
85
85
|
- lib/has_secure_passkey/options_for_create.rb
|
86
86
|
- lib/has_secure_passkey/options_for_get.rb
|
87
|
+
- lib/has_secure_passkey/recovery.rb
|
87
88
|
- lib/has_secure_passkey/version.rb
|
88
89
|
- lib/tasks/has_secure_passkey_tasks.rake
|
89
90
|
homepage: https://github.com/npezza93/has_secure_passkey
|
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
106
|
- !ruby/object:Gem::Version
|
106
107
|
version: '0'
|
107
108
|
requirements: []
|
108
|
-
rubygems_version: 3.
|
109
|
+
rubygems_version: 3.7.1
|
109
110
|
specification_version: 4
|
110
111
|
summary: Add passkey support to Rails
|
111
112
|
test_files: []
|