rotp 6.3.1 → 6.4.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/.github/workflows/release.yaml +63 -21
- data/.github/workflows/test.yaml +2 -3
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/README.md +5 -2
- data/lib/rotp/otp/uri.rb +7 -3
- data/lib/rotp/otp.rb +8 -2
- data/lib/rotp/version.rb +1 -1
- data/release-please-config.json +4 -1
- data/spec/lib/rotp/hotp_spec.rb +1 -1
- data/spec/lib/rotp/otp/uri_spec.rb +39 -0
- data/spec/lib/rotp/totp_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19178f891005be16f5ade5f33a43c8cc77a38e1ccdd81ca62285b32cc2dd3090
|
|
4
|
+
data.tar.gz: 40f54eaf3c58f09af3c9fe55aa232c97a49a4df581f3293f04bd906f89f1df06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2ad58aa47f5781b2a15891a1cad38a3cab52e9b93a6e4b7ade3e0d18bce1e887d25369f3fdfd11e0752992cd78f66fccf2cc2cefcdb83671af4d73776eb3d3d
|
|
7
|
+
data.tar.gz: e3f243779f0dab586cb8307ebc40fed5ed4572d314b056a766a00baaa949971e085fb05557c23947715d5205e0048b0162554d69392e4772304e611fdceee8ce
|
|
@@ -2,35 +2,77 @@ name: Release
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches:
|
|
5
|
+
branches:
|
|
6
6
|
- 'main'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
tag:
|
|
10
|
+
description: 'Existing tag to build and publish (e.g. v6.3.1). Used to backfill a release that never reached RubyGems.'
|
|
11
|
+
required: false
|
|
12
|
+
type: string
|
|
13
|
+
skip_tests:
|
|
14
|
+
description: 'Skip the test suite before publishing (only for backfilling an already-tagged release).'
|
|
15
|
+
required: false
|
|
16
|
+
default: false
|
|
17
|
+
type: boolean
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write
|
|
21
|
+
pull-requests: write
|
|
22
|
+
|
|
7
23
|
jobs:
|
|
8
|
-
release:
|
|
24
|
+
release-please:
|
|
25
|
+
if: ${{ github.event_name == 'push' }}
|
|
9
26
|
runs-on: ubuntu-latest
|
|
27
|
+
outputs:
|
|
28
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
29
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
10
30
|
steps:
|
|
11
|
-
- uses:
|
|
31
|
+
- uses: googleapis/release-please-action@v4
|
|
12
32
|
id: release
|
|
13
33
|
with:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
config-file: release-please-config.json
|
|
35
|
+
manifest-file: .release-please-manifest.json
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: [release-please]
|
|
39
|
+
if: ${{ always() && ((github.event_name == 'workflow_dispatch' && inputs.tag != '') || needs.release-please.outputs.release_created == 'true') }}
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
timeout-minutes: 20
|
|
42
|
+
permissions:
|
|
43
|
+
contents: write
|
|
44
|
+
id-token: write
|
|
45
|
+
env:
|
|
46
|
+
TAG: ${{ inputs.tag || needs.release-please.outputs.tag_name }}
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v6
|
|
49
|
+
with:
|
|
50
|
+
ref: ${{ env.TAG }}
|
|
51
|
+
fetch-depth: 0
|
|
52
|
+
persist-credentials: false
|
|
53
|
+
- name: Set up Ruby
|
|
54
|
+
uses: ruby/setup-ruby@v1
|
|
22
55
|
with:
|
|
23
|
-
ruby-version: 3.
|
|
24
|
-
bundler-cache:
|
|
25
|
-
|
|
56
|
+
ruby-version: '3.3'
|
|
57
|
+
bundler-cache: ${{ !inputs.skip_tests }}
|
|
58
|
+
- name: Verify gem version matches tag
|
|
59
|
+
run: |
|
|
60
|
+
tag_version="${TAG#v}"
|
|
61
|
+
gem_version="$(ruby -Ilib -e "require 'rotp/version'; print ROTP::VERSION")"
|
|
62
|
+
echo "tag=$tag_version gem=$gem_version"
|
|
63
|
+
test "$tag_version" = "$gem_version"
|
|
26
64
|
- name: Run tests
|
|
65
|
+
if: ${{ !inputs.skip_tests }}
|
|
27
66
|
run: bundle exec rspec
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- name:
|
|
31
|
-
|
|
67
|
+
- name: Configure RubyGems credentials (trusted publishing)
|
|
68
|
+
uses: rubygems/configure-rubygems-credentials@v2.1.0
|
|
69
|
+
- name: Build and publish gem
|
|
70
|
+
run: |
|
|
71
|
+
gem build *.gemspec
|
|
72
|
+
gem push *.gem
|
|
73
|
+
- name: Attach gem to the GitHub release
|
|
32
74
|
env:
|
|
33
75
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
34
|
-
run:
|
|
35
|
-
|
|
36
|
-
gh release upload $
|
|
76
|
+
run: |
|
|
77
|
+
gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" --title "$TAG" --generate-notes
|
|
78
|
+
gh release upload "$TAG" *.gem --clobber
|
data/.github/workflows/test.yaml
CHANGED
|
@@ -12,11 +12,10 @@ jobs:
|
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
|
|
14
14
|
strategy:
|
|
15
|
-
# Include '3.5.0-preview1' to test compatibility with the upcoming Ruby 3.5 release.
|
|
16
15
|
matrix:
|
|
17
|
-
ruby-version: ['
|
|
16
|
+
ruby-version: ['4.0', '3.4', '3.3', '3.2', '3.0', '2.7', '2.3', 'truffleruby-head']
|
|
18
17
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@v6
|
|
20
19
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
21
20
|
uses: ruby/setup-ruby@v1
|
|
22
21
|
with:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.4.0](https://github.com/mdp/rotp/compare/v6.3.1...v6.4.0) (2026-10-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add skip_default_uri_params option to provisioning URIs ([16e873c](https://github.com/mdp/rotp/commit/16e873c68e0736f8ce019252985ad0e9fcce95f1))
|
|
9
|
+
|
|
3
10
|
## [6.3.1] (2025-06-03)
|
|
4
11
|
|
|
5
12
|
* Fix: Drop usage of OpenStruct to support Ruby 3.5
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Although this library will continue to be maintained, if you're implementing a 2
|
|
|
8
8
|
- [Webauthn Ruby Gem](https://github.com/cedarcode/webauthn-ruby)
|
|
9
9
|
- [Rails demo app with Webauthn](https://github.com/cedarcode/webauthn-rails-demo-app)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
13
|
# The Ruby One Time Password Library
|
|
14
14
|
|
|
@@ -17,7 +17,6 @@ Although this library will continue to be maintained, if you're implementing a 2
|
|
|
17
17
|
[](https://www.rubydoc.info/github/mdp/rotp/master)
|
|
18
18
|
[](https://github.com/mdp/rotp/blob/master/LICENSE)
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
A ruby library for generating and validating one time passwords (HOTP & TOTP) according to [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238).
|
|
22
21
|
|
|
23
22
|
ROTP is compatible with [Google Authenticator](https://github.com/google/google-authenticator) available for [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) and [iPhone](https://itunes.apple.com/en/app/google-authenticator/id388497605) and any other TOTP based implementations.
|
|
@@ -150,6 +149,10 @@ totp.provisioning_uri("alice@google.com") # => 'otpauth://totp/My%20Service:alic
|
|
|
150
149
|
|
|
151
150
|
hotp = ROTP::HOTP.new("base32secret3232", issuer: "My Service")
|
|
152
151
|
hotp.provisioning_uri("alice@google.com", 0) # => 'otpauth://hotp/My%20Service:alice%40google.com?secret=base32secret3232&issuer=My%20Service&counter=0'
|
|
152
|
+
|
|
153
|
+
# By default optional values will be skipped from URI. But you can force to specify all values in the URI by passing `skip_default_uri_params: false`
|
|
154
|
+
totp = ROTP::TOTP.new("base32secret3232", issuer: "My Service", skip_default_uri_params: false)
|
|
155
|
+
totp.provisioning_uri("alice@google.com") # => otpauth://totp/My%20Service:alice%40google.com?secret=base32secret3232&issuer=My%20Service&algorithm=SHA1&digits=6&period=30
|
|
153
156
|
```
|
|
154
157
|
|
|
155
158
|
This can then be rendered as a QR Code which the user can scan using their mobile phone and the appropriate application.
|
data/lib/rotp/otp/uri.rb
CHANGED
|
@@ -15,7 +15,7 @@ module ROTP
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def algorithm
|
|
18
|
-
return
|
|
18
|
+
return if skip_default_uri_params && @otp.digest == DEFAULT_DIGEST
|
|
19
19
|
|
|
20
20
|
@otp.digest.upcase
|
|
21
21
|
end
|
|
@@ -28,7 +28,7 @@ module ROTP
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def digits
|
|
31
|
-
return if @otp.digits == DEFAULT_DIGITS
|
|
31
|
+
return if skip_default_uri_params && @otp.digits == DEFAULT_DIGITS
|
|
32
32
|
|
|
33
33
|
@otp.digits
|
|
34
34
|
end
|
|
@@ -62,7 +62,7 @@ module ROTP
|
|
|
62
62
|
|
|
63
63
|
def period
|
|
64
64
|
return if @otp.is_a?(HOTP)
|
|
65
|
-
return if @otp.interval == DEFAULT_INTERVAL
|
|
65
|
+
return if skip_default_uri_params && @otp.interval == DEFAULT_INTERVAL
|
|
66
66
|
|
|
67
67
|
@otp.interval
|
|
68
68
|
end
|
|
@@ -73,6 +73,10 @@ module ROTP
|
|
|
73
73
|
when HOTP then 'hotp'
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
def skip_default_uri_params
|
|
78
|
+
@otp.skip_default_uri_params
|
|
79
|
+
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
78
82
|
end
|
data/lib/rotp/otp.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module ROTP
|
|
2
2
|
class OTP
|
|
3
|
-
attr_reader :secret, :digits, :digest, :name, :issuer, :provisioning_params
|
|
3
|
+
attr_reader :secret, :digits, :digest, :name, :issuer, :provisioning_params, :skip_default_uri_params
|
|
4
4
|
DEFAULT_DIGITS = 6
|
|
5
|
+
DEFAULT_DIGEST = 'sha1'
|
|
5
6
|
|
|
6
7
|
# @param [String] secret in the form of base32
|
|
7
8
|
# @option options digits [Integer] (6)
|
|
@@ -19,13 +20,18 @@ module ROTP
|
|
|
19
20
|
# @option options provisioning_params [Hash] ({})
|
|
20
21
|
# Additional non-standard params you may want appended to the
|
|
21
22
|
# provisioning URI. Ex. `image: 'https://example.com/icon.png'`
|
|
23
|
+
# @option options skip_default_uri_params [Boolean] (true)
|
|
24
|
+
# When true, parameters that match the RFC defaults (SHA1, 6 digits,
|
|
25
|
+
# 30 second period) are omitted from the provisioning URI. Set to
|
|
26
|
+
# false to always include them.
|
|
22
27
|
# @returns [OTP] OTP instantiation
|
|
23
28
|
def initialize(s, options = {})
|
|
24
29
|
@digits = options[:digits] || DEFAULT_DIGITS
|
|
25
|
-
@digest = options[:digest] ||
|
|
30
|
+
@digest = options[:digest] || DEFAULT_DIGEST
|
|
26
31
|
@name = options[:name]
|
|
27
32
|
@issuer = options[:issuer]
|
|
28
33
|
@provisioning_params = options[:provisioning_params] || {}
|
|
34
|
+
@skip_default_uri_params = options.fetch(:skip_default_uri_params, true)
|
|
29
35
|
@secret = s
|
|
30
36
|
end
|
|
31
37
|
|
data/lib/rotp/version.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"release-type": "ruby",
|
|
2
3
|
"packages": {
|
|
3
4
|
".": {
|
|
5
|
+
"release-type": "ruby",
|
|
6
|
+
"version-file": "lib/rotp/version.rb",
|
|
4
7
|
"changelog-path": "CHANGELOG.md",
|
|
5
8
|
"bump-minor-pre-major": false,
|
|
6
9
|
"bump-patch-for-minor-pre-major": false,
|
|
@@ -9,4 +12,4 @@
|
|
|
9
12
|
}
|
|
10
13
|
},
|
|
11
14
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
12
|
-
}
|
|
15
|
+
}
|
data/spec/lib/rotp/hotp_spec.rb
CHANGED
|
@@ -110,7 +110,7 @@ RSpec.describe ROTP::HOTP do
|
|
|
110
110
|
|
|
111
111
|
describe '#provisioning_uri' do
|
|
112
112
|
let(:hotp) { ROTP::HOTP.new('a' * 32, name: "m@mdp.im") }
|
|
113
|
-
let(:params) {
|
|
113
|
+
let(:params) { query_params_for(uri) }
|
|
114
114
|
|
|
115
115
|
it 'created from the otp instance data' do
|
|
116
116
|
expect(hotp.provisioning_uri())
|
|
@@ -37,6 +37,12 @@ RSpec.describe ROTP::OTP::URI do
|
|
|
37
37
|
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512'
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
it 'includes an uppercase non-default digest' do
|
|
41
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'SHA256')
|
|
42
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
43
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256'
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
it 'includes non-default 8 digits' do
|
|
41
47
|
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digits: 8)
|
|
42
48
|
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
@@ -96,4 +102,37 @@ RSpec.describe ROTP::OTP::URI do
|
|
|
96
102
|
uri = described_class.new(otp, account_name: 'alice+1234@google.com')
|
|
97
103
|
expect(uri.to_s).to eq 'otpauth://totp/alice%2B1234%40google.com?secret=JBSWY3DPEHPK3PXP'
|
|
98
104
|
end
|
|
105
|
+
|
|
106
|
+
context 'with skip_default_uri_params' do
|
|
107
|
+
it 'excludes default SHA1 algorithm' do
|
|
108
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha1', skip_default_uri_params: true)
|
|
109
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
110
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'excludes default 6 digits' do
|
|
114
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digits: 6, skip_default_uri_params: true)
|
|
115
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
116
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'excludes default 30 second period' do
|
|
120
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', interval: 30, skip_default_uri_params: true)
|
|
121
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
122
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'includes all parameters when skip_default_uri_params is false' do
|
|
126
|
+
otp = ROTP::TOTP.new(
|
|
127
|
+
'JBSWY3DPEHPK3PXP',
|
|
128
|
+
digest: 'sha1',
|
|
129
|
+
digits: 6,
|
|
130
|
+
interval: 30,
|
|
131
|
+
issuer: 'ACME Co',
|
|
132
|
+
skip_default_uri_params: false
|
|
133
|
+
)
|
|
134
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
|
135
|
+
expect(uri.to_s).to eq 'otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30'
|
|
136
|
+
end
|
|
137
|
+
end
|
|
99
138
|
end
|
data/spec/lib/rotp/totp_spec.rb
CHANGED
|
@@ -223,7 +223,7 @@ RSpec.describe ROTP::TOTP do
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
describe '#provisioning_uri' do
|
|
226
|
-
let(:params) {
|
|
226
|
+
let(:params) { query_params_for(uri) }
|
|
227
227
|
|
|
228
228
|
context "with a provided name on the TOTP instance" do
|
|
229
229
|
let(:totp) { ROTP::TOTP.new(TEST_SECRET, name: "m@mdp.im") }
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,6 +3,7 @@ SimpleCov.start do
|
|
|
3
3
|
add_filter '/spec/'
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
+
require 'uri'
|
|
6
7
|
require 'rotp'
|
|
7
8
|
require 'timecop'
|
|
8
9
|
|
|
@@ -17,4 +18,10 @@ RSpec.configure do |config|
|
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
def query_params_for(uri)
|
|
22
|
+
URI.decode_www_form(URI.parse(uri).query).each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(key, value), params|
|
|
23
|
+
params[key] << value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
require_relative '../lib/rotp'
|