simple_auth-magic_link 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1db6e4dd1f065742e9b94a0e1c617b26fb72546c8b492ba555563586c62d3bea
4
- data.tar.gz: 2cd9d5f2496cdd1f38acc16b01dd218caba4ffca1f983118939fc6b85455102f
3
+ metadata.gz: 7735f278bf1499620fa1b4602dd88b477f54fd701bd09ada4236571f8c496d9d
4
+ data.tar.gz: b532899b3f5222fb78010dc54d67b63fd1514e298f1177f4581c3294e9aaea5d
5
5
  SHA512:
6
- metadata.gz: 76dcc931ee1084fae469bef697184761b644249ff636735dcaa3bf124d1966cea33b5d6ef2d47f46210b0a74be3bfed8bdf5d70c8cdfd5cf5260ffc8645f1088
7
- data.tar.gz: 97f72c2ab9dcf34c7c360798c2421ec99c9d8e496614882b5dbb1f23619b597ade34042888ace29fb94558892ebfa86282387006116b59ff064f71b73ff579b8
6
+ metadata.gz: 418ec9e52d1e9bc879440599533a1360b982fc6589208858fb5627e98f0cc3be25fea052eed4f0ee173566138ec6a45ae32c0ff9c897abd8d239a26dc12311cc
7
+ data.tar.gz: 99265424767eed276c8afbf07fd8567b51e2245ae14e51a3e26718f18cfd0be92d6edb249428bcd0c8fdf4112b6c5939bc5221a672abd2479efa73216db6f06f
data/README.md CHANGED
@@ -35,14 +35,14 @@ setting these options directly to `SimpleAuth::MagicLink`:
35
35
 
36
36
  ```ruby
37
37
  SimpleAuth::MagicLink.tap do |magic_link|
38
- # Optional. By default uses <https://github.com/fnando/haikunate>
39
- magic_link.code = -> { SecureRandom.hex(10) }
38
+ # Optional. By default 6 random numbers.
39
+ magic_link.code = > { Array.new(6) { SecureRandom.random_number(0..9) }.join }
40
40
 
41
41
  # Optional. By default, links expires 3 minutes from now.
42
42
  magic_link.ttl = 1.minute
43
43
 
44
44
  # Optional. By default, uses "default".
45
- magic_link.purpose = :generic
45
+ magic_link.purpose = :default
46
46
 
47
47
  # Required. The lambda that will be used to generate the magic link.
48
48
  # This will require the default url options like in
@@ -71,20 +71,28 @@ Then, you can create magic links by using
71
71
 
72
72
  After you create a link, you can send it by email by using `magic_link.url`.
73
73
 
74
+ > [!IMPORTANT]
75
+ >
76
+ > Save `magic_link.id` on the session. This id will be used later on to ensure
77
+ > that the link was generated on the same browser/device.
78
+
74
79
  To verify the magic link, you can use
75
- `email = SimpleAuth::MagicLink.verify(request.original_url, **options)`, where
76
- options can be:
80
+ `email = SimpleAuth::MagicLink.verify(url: request.original_url, id: session[:magic_link_id], **options)`,
81
+ where options can be:
77
82
 
78
83
  - `purpose`: required. The purpose of the code being verified.
79
84
 
80
85
  If the url is valid (i.e. it hasn't been tempered and it hasn't expired), then
81
- you'll get the email tied to the token back. Otherwise, you'll get `nil`. Notice
82
- that verified tokens are automatically removed upon verification.
86
+ you'll get the email tied to the token back. Otherwise, you'll get `nil`.
87
+
88
+ > [!INFO]
89
+ >
90
+ > Verified magic links are automatically removed upon verification.
83
91
 
84
92
  You can also verify the magic link by using just the code (maybe you sent this
85
93
  by SMS instead). In this case, you need to call something like
86
- `email = SimpleAuth::MagicLink.verify_code(code, **options)`. It expects the
87
- same options as `SimpleAuth::MagicLink.verify`.
94
+ `email = SimpleAuth::MagicLink.verify_code(code:, id: session[:magic_link_id], **options)`.
95
+ It expects the same options as `SimpleAuth::MagicLink.verify`.
88
96
 
89
97
  To remove expires links, use `SimpleAuth::MagicLink.clean!`.
90
98
 
@@ -94,9 +102,10 @@ something like this:
94
102
 
95
103
  1. User fills in log-in form with email address
96
104
  2. You call `magic_link = SimpleAuth::MagicLink.create!(email: params[:email])`
97
- 3. You then pass this in to your mailer with something like
105
+ 3. You persist the magic link id with `session[:magic_link_id] = magic_link.id`
106
+ 4. You then pass this in to your mailer with something like
98
107
  `Mailer.login(magic_link).send_later`
99
- 4. On your mailer, you can then have access to the user email via
108
+ 5. On your mailer, you can then have access to the user email via
100
109
  `magic_link.email`, the code via `magic_link.code` and the signed url via
101
110
  `magic_link.url`.
102
111
 
@@ -2,13 +2,32 @@
2
2
 
3
3
  module SimpleAuth
4
4
  module MagicLink
5
- class CodeVerifier < Verifier
6
- def self.call(code, purpose, time)
7
- new.call(code, purpose, time)
5
+ class CodeVerifier
6
+ def self.call(id:, code:, purpose:, time:)
7
+ new.call(id:, code:, purpose:, time:)
8
8
  end
9
9
 
10
- def call(code, purpose, time)
11
- verify_model(code, purpose, time)
10
+ def call(id:, code:, purpose:, time:)
11
+ verify_model(wanted_id: id, actual_code: code, purpose:, time:)
12
+ end
13
+
14
+ def secure_compare(a, b)
15
+ ActiveSupport::SecurityUtils.secure_compare(a, b)
16
+ end
17
+
18
+ def verify_model(wanted_id:, actual_code:, purpose:, time:)
19
+ magic_link = MagicLink.model.find_by(id: wanted_id)
20
+
21
+ return unless magic_link
22
+ return unless secure_compare(magic_link.code, actual_code.to_s)
23
+ return unless magic_link.expires_at >= time
24
+ return unless secure_compare(magic_link.purpose, purpose.to_s)
25
+
26
+ return unless magic_link
27
+
28
+ magic_link.destroy!
29
+
30
+ magic_link.email
12
31
  end
13
32
  end
14
33
  end
@@ -16,7 +16,7 @@ module SimpleAuth
16
16
  url,
17
17
  key: MagicLink.keyring.current_key.encryption_key,
18
18
  expires: expires_at,
19
- params: {code:, purpose:}
19
+ params: {id:}
20
20
  )
21
21
  end
22
22
  end
@@ -3,28 +3,31 @@
3
3
  module SimpleAuth
4
4
  module MagicLink
5
5
  class Verifier
6
- def self.call(url, purpose, time)
7
- new.call(url, purpose, time)
6
+ def self.call(id:, url:, purpose:, time:)
7
+ new.call(id:, url:, purpose:, time:)
8
8
  end
9
9
 
10
- def call(url, purpose, time)
10
+ def call(id:, url:, purpose:, time:)
11
11
  return unless SignedURL.verified?(url, key: MagicLink.encryption_key)
12
12
 
13
13
  uri = URI(url)
14
14
  params = Rack::Utils.parse_query(uri.query)
15
- code = params.fetch("code")
15
+ actual_id = params.fetch("id")
16
16
 
17
- verify_model(code, purpose, time)
17
+ verify_model(wanted_id: id, actual_id:, purpose:, time:)
18
18
  end
19
19
 
20
- def verify_model(code, purpose, time)
21
- magic_link = MagicLink
22
- .model
23
- .where("expires_at >= ?", time)
24
- .find_by(
25
- code_digest: MagicLink.keyring.digest(code),
26
- purpose:
27
- )
20
+ def secure_compare(a, b)
21
+ ActiveSupport::SecurityUtils.secure_compare(a, b)
22
+ end
23
+
24
+ def verify_model(wanted_id:, actual_id:, purpose:, time:)
25
+ magic_link = MagicLink.model.find_by(id: wanted_id)
26
+
27
+ return unless magic_link
28
+ return unless secure_compare(magic_link.id.to_s, actual_id.to_s)
29
+ return unless magic_link.expires_at >= time
30
+ return unless secure_compare(magic_link.purpose, purpose.to_s)
28
31
 
29
32
  return unless magic_link
30
33
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SimpleAuth
4
4
  module MagicLink
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.2"
6
6
  end
7
7
  end
@@ -3,7 +3,6 @@
3
3
  require "active_record"
4
4
  require "attr_keyring"
5
5
  require "defaults"
6
- require "haikunate"
7
6
  require "url_signature"
8
7
  require "rails/engine"
9
8
 
@@ -22,9 +21,7 @@ module SimpleAuth
22
21
  end
23
22
 
24
23
  def self.default_code
25
- lambda do
26
- Haikunate.call(variant: -> { SecureRandom.hex(3) })
27
- end
24
+ -> { Array.new(6) { SecureRandom.random_number(0..9) }.join }
28
25
  end
29
26
 
30
27
  def self.encryption_key
@@ -54,22 +51,29 @@ module SimpleAuth
54
51
  )
55
52
  end
56
53
 
54
+ # Verify whether a url is valid or not.
55
+ # The provided `id` must match whatever is set on the url.
56
+ # Ideally, this `id` is saved on the user's session and compared whenever
57
+ # the user visits the link. This is the only way you can ensure that the
58
+ # url hasn't be generated on a different device.
57
59
  def self.verify(
58
- url,
60
+ id:,
61
+ url:,
59
62
  time: Time.current,
60
63
  purpose: MagicLink.purpose,
61
64
  verifier: Verifier
62
65
  )
63
- verifier.call(url, purpose, time)
66
+ verifier.call(id:, url:, purpose:, time:)
64
67
  end
65
68
 
66
69
  def self.verify_code(
67
- code,
70
+ id:,
71
+ code:,
68
72
  time: Time.current,
69
73
  purpose: MagicLink.purpose,
70
74
  verifier: CodeVerifier
71
75
  )
72
- verifier.call(code, purpose, time)
76
+ verifier.call(id:, code:, purpose:, time:)
73
77
  end
74
78
 
75
79
  def self.restore_defaults!
@@ -38,17 +38,15 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "activerecord"
39
39
  spec.add_dependency "attr_keyring"
40
40
  spec.add_dependency "defaults"
41
- spec.add_dependency "haikunate"
42
41
  spec.add_dependency "simple_auth"
43
42
  spec.add_dependency "url_signature"
44
43
  spec.add_development_dependency "minitest"
45
44
  spec.add_development_dependency "minitest-utils"
46
45
  spec.add_development_dependency "mocha"
47
- spec.add_development_dependency "pry-meta"
48
46
  spec.add_development_dependency "rails"
49
47
  spec.add_development_dependency "rake"
50
48
  spec.add_development_dependency "rubocop"
51
49
  spec.add_development_dependency "rubocop-fnando"
52
50
  spec.add_development_dependency "simplecov"
53
- spec.add_development_dependency "sqlite3"
51
+ spec.add_development_dependency "sqlite3", "~> 1.7"
54
52
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_auth-magic_link
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - me@fnando.com
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-01 00:00:00.000000000 Z
11
+ date: 2024-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: haikunate
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: simple_auth
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -136,20 +122,6 @@ dependencies:
136
122
  - - ">="
137
123
  - !ruby/object:Gem::Version
138
124
  version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: pry-meta
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
125
  - !ruby/object:Gem::Dependency
154
126
  name: rails
155
127
  requirement: !ruby/object:Gem::Requirement
@@ -224,16 +196,16 @@ dependencies:
224
196
  name: sqlite3
225
197
  requirement: !ruby/object:Gem::Requirement
226
198
  requirements:
227
- - - ">="
199
+ - - "~>"
228
200
  - !ruby/object:Gem::Version
229
- version: '0'
201
+ version: '1.7'
230
202
  type: :development
231
203
  prerelease: false
232
204
  version_requirements: !ruby/object:Gem::Requirement
233
205
  requirements:
234
- - - ">="
206
+ - - "~>"
235
207
  - !ruby/object:Gem::Version
236
- version: '0'
208
+ version: '1.7'
237
209
  description: Passwordless sign-in for simple_auth.
238
210
  email:
239
211
  - me@fnando.com
@@ -277,10 +249,10 @@ metadata:
277
249
  rubygems_mfa_required: 'true'
278
250
  homepage_uri: https://github.com/fnando/simple_auth-magic_link
279
251
  bug_tracker_uri: https://github.com/fnando/simple_auth-magic_link/issues
280
- source_code_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.1
281
- changelog_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.1/CHANGELOG.md
282
- documentation_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.1/README.md
283
- license_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.1/LICENSE.md
252
+ source_code_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.2
253
+ changelog_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.2/CHANGELOG.md
254
+ documentation_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.2/README.md
255
+ license_uri: https://github.com/fnando/simple_auth-magic_link/tree/v0.0.2/LICENSE.md
284
256
  post_install_message:
285
257
  rdoc_options: []
286
258
  require_paths:
@@ -296,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
296
268
  - !ruby/object:Gem::Version
297
269
  version: '0'
298
270
  requirements: []
299
- rubygems_version: 3.5.5
271
+ rubygems_version: 3.5.9
300
272
  signing_key:
301
273
  specification_version: 4
302
274
  summary: Passwordless sign-in for simple_auth.