simple_auth-magic_link 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1db6e4dd1f065742e9b94a0e1c617b26fb72546c8b492ba555563586c62d3bea
4
+ data.tar.gz: 2cd9d5f2496cdd1f38acc16b01dd218caba4ffca1f983118939fc6b85455102f
5
+ SHA512:
6
+ metadata.gz: 76dcc931ee1084fae469bef697184761b644249ff636735dcaa3bf124d1966cea33b5d6ef2d47f46210b0a74be3bfed8bdf5d70c8cdfd5cf5260ffc8645f1088
7
+ data.tar.gz: 97f72c2ab9dcf34c7c360798c2421ec99c9d8e496614882b5dbb1f23619b597ade34042888ace29fb94558892ebfa86282387006116b59ff064f71b73ff579b8
@@ -0,0 +1,4 @@
1
+ # You can read more about CODEOWNERS at
2
+ # https://help.github.com/github/creating-cloning-and-archiving-repositories/about-code-owners
3
+
4
+ * @fnando
@@ -0,0 +1,4 @@
1
+ # These are supported funding model platforms
2
+ ---
3
+ github: [fnando]
4
+ custom: ["https://paypal.me/nandovieira/๐Ÿ•"]
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: "๐Ÿ› Bug Report"
3
+ about: Report a reproducible bug or regression.
4
+ title: 'Bug: '
5
+ labels: 'Status: Unconfirmed'
6
+
7
+ ---
8
+
9
+ <!--
10
+ - Please provide a clear and concise description of what the bug is.
11
+ - If possible, add an example reproducing your issue.
12
+ - Please test using the latest version of simple_auth-magic_link
13
+ to make sure your issue has not already been fixed.
14
+ -->
15
+
16
+ ## Description
17
+
18
+ [Add bug description here]
19
+
20
+ ## How to reproduce
21
+
22
+ [Add steps on how to reproduce this issue]
23
+
24
+ ## What do you expect
25
+
26
+ [Describe what do you expect to happen]
27
+
28
+ ## What happened instead
29
+
30
+ [Describe the actual results]
31
+
32
+ ## Software:
33
+
34
+ - Gem version: [Add gem version here]
35
+ - Ruby version: [Add version here]
36
+
37
+ ## Full backtrace
38
+
39
+ ```text
40
+ [Paste full backtrace here]
41
+ ```
@@ -0,0 +1,5 @@
1
+ ---
2
+ contact_links:
3
+ - name: "๐Ÿคจ Q&A"
4
+ url: https://github.com/fnando/simple_auth-magic_link/discussions/new?category=q-a
5
+ about: Have a question? Ask it away here!
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: "๐Ÿ’ก Feature request"
3
+ about: Have an idea that may be useful? Make a suggestion!
4
+ title: 'Feature Request: '
5
+ labels: 'Feature request'
6
+
7
+ ---
8
+
9
+ ## Description
10
+
11
+ _A clear and concise description of what the problem is._
12
+
13
+ ## Describe the solution
14
+
15
+ _A clear and concise description of what you want to happen._
16
+
17
+ ## Alternatives you considered
18
+
19
+ _A clear and concise description of any alternative solutions or features you've considered._
20
+
21
+ ## Additional context
22
+
23
+ _Add any other context, screenshots, links, etc about the feature request here._
@@ -0,0 +1,38 @@
1
+ <!--
2
+ If you're making a doc PR or something tiny where the below is irrelevant,
3
+ delete this template and use a short description, but in your description aim to
4
+ include both what the change is, and why it is being made, with enough context
5
+ for anyone to understand.
6
+ -->
7
+
8
+ <details>
9
+ <summary>PR Checklist</summary>
10
+
11
+ ### PR Structure
12
+
13
+ - [ ] This PR has reasonably narrow scope (if not, break it down into smaller
14
+ PRs).
15
+ - [ ] This PR avoids mixing refactoring changes with feature changes (split into
16
+ two PRs otherwise).
17
+ - [ ] This PR's title starts is concise and descriptive.
18
+
19
+ ### Thoroughness
20
+
21
+ - [ ] This PR adds tests for the most critical parts of the new functionality or
22
+ fixes.
23
+ - [ ] I've updated any docs, `.md` files, etcโ€ฆ affected by this change.
24
+
25
+ </details>
26
+
27
+ ### What
28
+
29
+ [TODO: Short statement about what is changing.]
30
+
31
+ ### Why
32
+
33
+ [TODO: Why this change is being made. Include any context required to understand
34
+ the why.]
35
+
36
+ ### Known limitations
37
+
38
+ [TODO or N/A]
@@ -0,0 +1,15 @@
1
+ ---
2
+ # Documentation:
3
+ # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
4
+
5
+ version: 2
6
+ updates:
7
+ - package-ecosystem: "github-actions"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "daily"
11
+
12
+ - package-ecosystem: bundler
13
+ directory: "/"
14
+ schedule:
15
+ interval: "daily"
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: ruby-tests
3
+
4
+ on:
5
+ pull_request_target:
6
+ push:
7
+ branches:
8
+ - main
9
+ workflow_dispatch:
10
+ inputs: {}
11
+
12
+ jobs:
13
+ build:
14
+ name: Tests with Ruby ${{ matrix.ruby }} and ${{ matrix.gemfile }}
15
+ runs-on: "ubuntu-latest"
16
+ if: |
17
+ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request_target' ||
18
+ github.actor != 'dependabot[bot]'
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ ruby: ["2.7", "3.0", "3.1"]
23
+ gemfile:
24
+ - Gemfile
25
+
26
+ steps:
27
+ - uses: actions/checkout@v3
28
+
29
+ - uses: actions/cache@v3
30
+ with:
31
+ path: vendor/bundle
32
+ key: >
33
+ ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles(matrix.gemfile) }}
34
+
35
+ - name: Set up Ruby
36
+ uses: ruby/setup-ruby@v1
37
+ with:
38
+ ruby-version: ${{ matrix.ruby }}
39
+
40
+ - name: Install gem dependencies
41
+ env:
42
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
43
+ run: |
44
+ gem install bundler
45
+ bundle config path vendor/bundle
46
+ bundle update --jobs 4 --retry 3
47
+
48
+ - name: Run Tests
49
+ env:
50
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
51
+ run: |
52
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+ *.log
11
+ *.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ inherit_gem:
3
+ rubocop-fnando: .rubocop.yml
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.2
7
+ NewCops: enable
8
+ Exclude:
9
+ - vendor/**/*
10
+ - gemfiles/**/*
11
+ Naming/FileName:
12
+ Exclude:
13
+ - lib/simple_auth-magic_link.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ <!--
4
+ Prefix your message with one of the following:
5
+
6
+ - [Added] for new features.
7
+ - [Changed] for changes in existing functionality.
8
+ - [Deprecated] for soon-to-be removed features.
9
+ - [Removed] for now removed features.
10
+ - [Fixed] for any bug fixes.
11
+ - [Security] in case of vulnerabilities.
12
+ -->
13
+
14
+ ## Unreleased
15
+
16
+ - Initial release.
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at me@fnando.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,79 @@
1
+ # Contributing to simple_auth-magic_link
2
+
3
+ ๐Ÿ‘๐ŸŽ‰ First off, thanks for taking the time to contribute! ๐ŸŽ‰๐Ÿ‘
4
+
5
+ The following is a set of guidelines for contributing to this project. These are
6
+ mostly guidelines, not rules. Use your best judgment, and feel free to propose
7
+ changes to this document in a pull request.
8
+
9
+ ## Code of Conduct
10
+
11
+ Everyone interacting in this project's codebases, issue trackers, chat rooms and
12
+ mailing lists is expected to follow the [code of conduct](https://github.com/fnando/simple_auth-magic_link/blob/main/CODE_OF_CONDUCT.md).
13
+
14
+ ## Reporting bugs
15
+
16
+ This section guides you through submitting a bug report. Following these
17
+ guidelines helps maintainers and the community understand your report, reproduce
18
+ the behavior, and find related reports.
19
+
20
+ - Before creating bug reports, please check the open issues; somebody may
21
+ already have submitted something similar, and you may not need to create a new
22
+ one.
23
+ - When you are creating a bug report, please include as many details as
24
+ possible, with an example reproducing the issue.
25
+
26
+ ## Contributing with code
27
+
28
+ Before making any radicals changes, please make sure you discuss your intention
29
+ by [opening an issue on Github](https://github.com/fnando/simple_auth-magic_link/issues).
30
+
31
+ When you're ready to make your pull request, follow checklist below to make sure
32
+ your contribution is according to how this project works.
33
+
34
+ 1. [Fork](https://help.github.com/forking/) simple_auth-magic_link
35
+ 2. Create a topic branch - `git checkout -b my_branch`
36
+ 3. Make your changes using [descriptive commit messages](#commit-messages)
37
+ 4. Update CHANGELOG.md describing your changes by adding an entry to the
38
+ "Unreleased" section. If this section is not available, create one right
39
+ before the last version.
40
+ 5. Push to your branch - `git push origin my_branch`
41
+ 6. [Create a pull request](https://help.github.com/articles/creating-a-pull-request)
42
+ 7. That's it!
43
+
44
+ ## Styleguides
45
+
46
+ ### Commit messages
47
+
48
+ - Use the present tense ("Add feature" not "Added feature")
49
+ - Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
50
+ - Limit the first line to 72 characters or less
51
+ - Reference issues and pull requests liberally after the first line
52
+
53
+ ### Changelog
54
+
55
+ - Add a message describing your changes to the "Unreleased" section. The
56
+ changelog message should follow the same style as the commit message.
57
+ - Prefix your message with one of the following:
58
+ - `[Added]` for new features.
59
+ - `[Changed]` for changes in existing functionality.
60
+ - `[Deprecated]` for soon-to-be removed features.
61
+ - `[Removed]` for now removed features.
62
+ - `[Fixed]` for any bug fixes.
63
+ - `[Security]` in case of vulnerabilities.
64
+
65
+ ### Ruby code
66
+
67
+ - This project uses [Rubocop](https://rubocop.org) to enforce code style. Before
68
+ submitting your changes, make sure your tests are passing and code conforms to
69
+ the expected style by running `rake`.
70
+ - Do not change the library version. This will be done by the maintainer
71
+ whenever a new version is about to be released.
72
+
73
+ ### JavaScript code
74
+
75
+ - This project uses [ESLint](https://eslint.org) to enforce code style. Before
76
+ submitting your changes, make sure your tests are passing and code conforms to
77
+ the expected style by running `yarn test:ci`.
78
+ - Do not change the library version. This will be done by the maintainer
79
+ whenever a new version is about to be released.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 me@fnando.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # simple_auth-magic_link
2
+
3
+ [![Tests](https://github.com/fnando/simple_auth-magic_link/workflows/ruby-tests/badge.svg)](https://github.com/fnando/simple_auth-magic_link)
4
+ [![Gem](https://img.shields.io/gem/v/simple_auth-magic_link.svg)](https://rubygems.org/gems/simple_auth-magic_link)
5
+ [![Gem](https://img.shields.io/gem/dt/simple_auth-magic_link.svg)](https://rubygems.org/gems/simple_auth-magic_link)
6
+ [![MIT License](https://img.shields.io/:License-MIT-blue.svg)](https://tldrlegal.com/license/mit-license)
7
+
8
+ Passwordless authentication for
9
+ [simple_auth](https://github.com/fnando/simple_auth).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ gem install simple_auth-magic_link
15
+ ```
16
+
17
+ Or add the following line to your project's Gemfile:
18
+
19
+ ```ruby
20
+ gem "simple_auth-magic_link"
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ First, you need to copy the migration files and apply the migrations to your
26
+ database.
27
+
28
+ ```console
29
+ $ rails simple_auth_magic_link_engine:install:migrations
30
+ $ rails db:migrate
31
+ ```
32
+
33
+ You can configure the magic link's ttl, code generator and default purpose by
34
+ setting these options directly to `SimpleAuth::MagicLink`:
35
+
36
+ ```ruby
37
+ SimpleAuth::MagicLink.tap do |magic_link|
38
+ # Optional. By default uses <https://github.com/fnando/haikunate>
39
+ magic_link.code = -> { SecureRandom.hex(10) }
40
+
41
+ # Optional. By default, links expires 3 minutes from now.
42
+ magic_link.ttl = 1.minute
43
+
44
+ # Optional. By default, uses "default".
45
+ magic_link.purpose = :generic
46
+
47
+ # Required. The lambda that will be used to generate the magic link.
48
+ # This will require the default url options like in
49
+ # `Rails.application.routes.default_url_options = {host: "example.com"}`
50
+ magic_link.url = -> { verify_email_url }
51
+
52
+ # Required. The keyring that will be used to encrypt the user's email.
53
+ # Generate the keyring secret and digest salt with the following command:
54
+ # dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
55
+ keyring = {"1" => "<32 bytes>"}
56
+ magic_link.attr_keyring keyring, digest_salt: "<32 bytes>"
57
+ end
58
+ ```
59
+
60
+ Then, you can create magic links by using
61
+ `magic_link = SimpleAuth::MagicLink.create!(options)`, where `options` can be:
62
+
63
+ - `email`: required. The email tied to this magic link.
64
+ - `purpose`: optional. A string that identifies the purpose of the magic link.
65
+ Defaults to `default`. You can use this to discern links that will be used for
66
+ other purposes (e.g. confirm an action, login, signup, etc).
67
+ - `expires_at`: optional. The code's expiration time. Defaults to three minutes
68
+ from now.
69
+ - `code`: optional. The code that tied to this magic link. Defaults to
70
+ [haikunate](https://github.com/fnando/haikunate).
71
+
72
+ After you create a link, you can send it by email by using `magic_link.url`.
73
+
74
+ To verify the magic link, you can use
75
+ `email = SimpleAuth::MagicLink.verify(request.original_url, **options)`, where
76
+ options can be:
77
+
78
+ - `purpose`: required. The purpose of the code being verified.
79
+
80
+ 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.
83
+
84
+ You can also verify the magic link by using just the code (maybe you sent this
85
+ 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`.
88
+
89
+ To remove expires links, use `SimpleAuth::MagicLink.clean!`.
90
+
91
+ Notice that this plugin doesn't implement any mailers, so you'll need to handle
92
+ that yourself. For example, a complete flow for a login/signup process would be
93
+ something like this:
94
+
95
+ 1. User fills in log-in form with email address
96
+ 2. You call `magic_link = SimpleAuth::MagicLink.create!(email: params[:email])`
97
+ 3. You then pass this in to your mailer with something like
98
+ `Mailer.login(magic_link).send_later`
99
+ 4. On your mailer, you can then have access to the user email via
100
+ `magic_link.email`, the code via `magic_link.code` and the signed url via
101
+ `magic_link.url`.
102
+
103
+ ## Maintainer
104
+
105
+ - [me@fnando.com](https://github.com/fnando)
106
+
107
+ ## Contributors
108
+
109
+ - <https://github.com/fnando/simple_auth-magic_link/contributors>
110
+
111
+ ## Contributing
112
+
113
+ For more details about how to contribute, please read
114
+ <https://github.com/fnando/simple_auth-magic_link/blob/main/CONTRIBUTING.md>.
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the
119
+ [MIT License](https://opensource.org/licenses/MIT). A copy of the license can be
120
+ found at
121
+ <https://github.com/fnando/simple_auth-magic_link/blob/main/LICENSE.md>.
122
+
123
+ ## Code of Conduct
124
+
125
+ Everyone interacting in the simple_auth-magic_link project's codebases, issue
126
+ trackers, chat rooms and mailing lists is expected to follow the
127
+ [code of conduct](https://github.com/fnando/simple_auth-magic_link/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "simple_auth-magic_link"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ begin
11
+ require "pry"
12
+ Pry.start
13
+ rescue LoadError
14
+ require "irb"
15
+ IRB.start(__FILE__)
16
+ end
data/bin/setup ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ IFS=$'\n\t'
6
+ set -vx
7
+
8
+ bundle install
9
+
10
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateMagicLinks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :magic_links do |t|
6
+ t.integer :keyring_id, null: false
7
+ t.text :code_digest, null: false
8
+ t.text :email_digest, null: false
9
+ t.text :purpose, null: false
10
+ t.binary :encrypted_code, null: false
11
+ t.binary :encrypted_email, null: false
12
+ t.datetime :expires_at, null: false
13
+ t.datetime :created_at, null: false
14
+ end
15
+
16
+ add_index :magic_links, :code_digest, unique: true
17
+ add_index :magic_links, %i[purpose email_digest], unique: true
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuth
4
+ module MagicLink
5
+ class CodeVerifier < Verifier
6
+ def self.call(code, purpose, time)
7
+ new.call(code, purpose, time)
8
+ end
9
+
10
+ def call(code, purpose, time)
11
+ verify_model(code, purpose, time)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuth
4
+ module MagicLink
5
+ class Engine < Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuth
4
+ module MagicLink
5
+ class Model < ActiveRecord::Base
6
+ include AttrKeyring.active_record
7
+
8
+ self.table_name = "magic_links"
9
+
10
+ attr_encrypt :code, :email
11
+
12
+ def url
13
+ url = Rails.application.routes.url_helpers.instance_eval(&MagicLink.url)
14
+
15
+ SignedURL.call(
16
+ url,
17
+ key: MagicLink.keyring.current_key.encryption_key,
18
+ expires: expires_at,
19
+ params: {code:, purpose:}
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateMagicLinks < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
4
+ def change
5
+ create_table :magic_links do |t|
6
+ t.integer :keyring_id, null: false
7
+ t.text :code_digest, null: false
8
+ t.binary :encrypted_code, null: false
9
+ t.datetime :expires_at, null: false
10
+ t.datetime :created_at, null: false
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuth
4
+ module MagicLink
5
+ class Verifier
6
+ def self.call(url, purpose, time)
7
+ new.call(url, purpose, time)
8
+ end
9
+
10
+ def call(url, purpose, time)
11
+ return unless SignedURL.verified?(url, key: MagicLink.encryption_key)
12
+
13
+ uri = URI(url)
14
+ params = Rack::Utils.parse_query(uri.query)
15
+ code = params.fetch("code")
16
+
17
+ verify_model(code, purpose, time)
18
+ end
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
+ )
28
+
29
+ return unless magic_link
30
+
31
+ magic_link.destroy!
32
+
33
+ magic_link.email
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuth
4
+ module MagicLink
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "attr_keyring"
5
+ require "defaults"
6
+ require "haikunate"
7
+ require "url_signature"
8
+ require "rails/engine"
9
+
10
+ require_relative "magic_link/engine"
11
+ require_relative "magic_link/model"
12
+ require_relative "magic_link/version"
13
+ require_relative "magic_link/verifier"
14
+ require_relative "magic_link/code_verifier"
15
+
16
+ module SimpleAuth
17
+ module MagicLink
18
+ class << self
19
+ attr_accessor :code, :ttl, :purpose, :url
20
+
21
+ delegate :attr_keyring, :keyring, to: :model
22
+ end
23
+
24
+ def self.default_code
25
+ lambda do
26
+ Haikunate.call(variant: -> { SecureRandom.hex(3) })
27
+ end
28
+ end
29
+
30
+ def self.encryption_key
31
+ MagicLink.keyring.current_key.encryption_key
32
+ end
33
+
34
+ def self.model
35
+ Model
36
+ end
37
+
38
+ def self.create!(
39
+ email:,
40
+ code: MagicLink.code.call,
41
+ purpose: MagicLink.purpose,
42
+ expires_at: Time.now.utc + MagicLink.ttl
43
+ )
44
+ model.where(
45
+ email_digest: model.keyring.digest(email),
46
+ purpose:
47
+ ).delete_all
48
+
49
+ model.create!(
50
+ email:,
51
+ code:,
52
+ purpose:,
53
+ expires_at:
54
+ )
55
+ end
56
+
57
+ def self.verify(
58
+ url,
59
+ time: Time.current,
60
+ purpose: MagicLink.purpose,
61
+ verifier: Verifier
62
+ )
63
+ verifier.call(url, purpose, time)
64
+ end
65
+
66
+ def self.verify_code(
67
+ code,
68
+ time: Time.current,
69
+ purpose: MagicLink.purpose,
70
+ verifier: CodeVerifier
71
+ )
72
+ verifier.call(code, purpose, time)
73
+ end
74
+
75
+ def self.restore_defaults!
76
+ self.code = default_code
77
+ self.ttl = 3.minutes
78
+ self.purpose = "default"
79
+ self.url = proc { raise "please set the url" }
80
+ end
81
+
82
+ def self.clean!
83
+ model.where("expires_at < ?", Time.current).delete_all
84
+ end
85
+
86
+ restore_defaults!
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_auth/magic_link"
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/simple_auth/magic_link/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "simple_auth-magic_link"
7
+ spec.version = SimpleAuth::MagicLink::VERSION
8
+ spec.authors = ["me@fnando.com"]
9
+ spec.email = ["me@fnando.com"]
10
+ spec.metadata = {"rubygems_mfa_required" => "true"}
11
+
12
+ spec.summary = "Passwordless sign-in for simple_auth."
13
+ spec.description = spec.summary
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
16
+
17
+ github_url = "https://github.com/fnando/simple_auth-magic_link"
18
+ github_tree_url = "#{github_url}/tree/v#{spec.version}"
19
+
20
+ spec.homepage = github_url
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["bug_tracker_uri"] = "#{github_url}/issues"
23
+ spec.metadata["source_code_uri"] = github_tree_url
24
+ spec.metadata["changelog_uri"] = "#{github_tree_url}/CHANGELOG.md"
25
+ spec.metadata["documentation_uri"] = "#{github_tree_url}/README.md"
26
+ spec.metadata["license_uri"] = "#{github_tree_url}/LICENSE.md"
27
+
28
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
+ `git ls-files -z`
30
+ .split("\x0")
31
+ .reject {|f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_dependency "activerecord"
39
+ spec.add_dependency "attr_keyring"
40
+ spec.add_dependency "defaults"
41
+ spec.add_dependency "haikunate"
42
+ spec.add_dependency "simple_auth"
43
+ spec.add_dependency "url_signature"
44
+ spec.add_development_dependency "minitest"
45
+ spec.add_development_dependency "minitest-utils"
46
+ spec.add_development_dependency "mocha"
47
+ spec.add_development_dependency "pry-meta"
48
+ spec.add_development_dependency "rails"
49
+ spec.add_development_dependency "rake"
50
+ spec.add_development_dependency "rubocop"
51
+ spec.add_development_dependency "rubocop-fnando"
52
+ spec.add_development_dependency "simplecov"
53
+ spec.add_development_dependency "sqlite3"
54
+ end
metadata ADDED
@@ -0,0 +1,303 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_auth-magic_link
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - me@fnando.com
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: attr_keyring
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: defaults
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
70
+ name: simple_auth
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: url_signature
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest-utils
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: mocha
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ 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
+ - !ruby/object:Gem::Dependency
154
+ name: rails
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rake
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubocop-fnando
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: simplecov
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: sqlite3
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ description: Passwordless sign-in for simple_auth.
238
+ email:
239
+ - me@fnando.com
240
+ executables: []
241
+ extensions: []
242
+ extra_rdoc_files: []
243
+ files:
244
+ - ".github/CODEOWNERS"
245
+ - ".github/FUNDING.yml"
246
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
247
+ - ".github/ISSUE_TEMPLATE/config.yml"
248
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
249
+ - ".github/PULL_REQUEST_TEMPLATE.md"
250
+ - ".github/dependabot.yml"
251
+ - ".github/workflows/ruby-tests.yml"
252
+ - ".gitignore"
253
+ - ".rubocop.yml"
254
+ - CHANGELOG.md
255
+ - CODE_OF_CONDUCT.md
256
+ - CONTRIBUTING.md
257
+ - Gemfile
258
+ - LICENSE.md
259
+ - README.md
260
+ - Rakefile
261
+ - bin/console
262
+ - bin/setup
263
+ - db/migrate/001_create_magic_links.rb
264
+ - lib/simple_auth-magic_link.rb
265
+ - lib/simple_auth/magic_link.rb
266
+ - lib/simple_auth/magic_link/code_verifier.rb
267
+ - lib/simple_auth/magic_link/engine.rb
268
+ - lib/simple_auth/magic_link/model.rb
269
+ - lib/simple_auth/magic_link/templates/migration.erb
270
+ - lib/simple_auth/magic_link/verifier.rb
271
+ - lib/simple_auth/magic_link/version.rb
272
+ - simple_auth-magic_link.gemspec
273
+ homepage: https://github.com/fnando/simple_auth-magic_link
274
+ licenses:
275
+ - MIT
276
+ metadata:
277
+ rubygems_mfa_required: 'true'
278
+ homepage_uri: https://github.com/fnando/simple_auth-magic_link
279
+ 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
284
+ post_install_message:
285
+ rdoc_options: []
286
+ require_paths:
287
+ - lib
288
+ required_ruby_version: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: 3.2.0
293
+ required_rubygems_version: !ruby/object:Gem::Requirement
294
+ requirements:
295
+ - - ">="
296
+ - !ruby/object:Gem::Version
297
+ version: '0'
298
+ requirements: []
299
+ rubygems_version: 3.5.5
300
+ signing_key:
301
+ specification_version: 4
302
+ summary: Passwordless sign-in for simple_auth.
303
+ test_files: []