github-authentication 0.1.0 → 1.0.2
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/.rubocop.yml +4 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +19 -7
- data/README.md +20 -12
- data/bin/console +1 -1
- data/docs/github_credential_helper.md +40 -0
- data/exe/git-credential-github-app +32 -0
- data/github-authentication.gemspec +5 -4
- data/lib/github-authentication.rb +3 -0
- data/lib/github_authentication/cache.rb +26 -0
- data/lib/github_authentication/generator/app.rb +50 -0
- data/lib/github_authentication/generator/personal.rb +18 -0
- data/lib/github_authentication/generator.rb +11 -0
- data/lib/github_authentication/git_credential_helper.rb +56 -0
- data/lib/github_authentication/http.rb +32 -0
- data/lib/github_authentication/object_cache.rb +34 -0
- data/lib/github_authentication/provider.rb +54 -0
- data/lib/github_authentication/retriable.rb +50 -0
- data/lib/github_authentication/token.rb +55 -0
- data/lib/github_authentication/version.rb +5 -0
- data/lib/github_authentication.rb +11 -0
- metadata +36 -30
- data/lib/github/authentication/cache.rb +0 -28
- data/lib/github/authentication/generator/app.rb +0 -47
- data/lib/github/authentication/generator/personal.rb +0 -20
- data/lib/github/authentication/generator.rb +0 -4
- data/lib/github/authentication/http.rb +0 -34
- data/lib/github/authentication/object_cache.rb +0 -36
- data/lib/github/authentication/provider.rb +0 -56
- data/lib/github/authentication/retriable.rb +0 -52
- data/lib/github/authentication/token.rb +0 -57
- data/lib/github/authentication/version.rb +0 -7
- data/lib/github/authentication.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a279af637a4369feb5bff421697bed089b09f8096d6829eccb37b3c4911d7ba9
|
4
|
+
data.tar.gz: edb60c288c631aafd17fbc0b361fef03f86e019a38a77d1debcc1544627e424d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10259c8db9c441f9d899962653a0307cc0eed4584797135857c883a047c4609027909cf7bfdc2828ebe6b3b9f520244cdf4267e0fa8957fa2daea46795bbc83b
|
7
|
+
data.tar.gz: f6d4ab126dbc799ec03fea9a6d7c5893d4abae73fb50b16cb3a15169876f615ee01756593d21d1b86f7cadf88b8630faa6f7fd7ca7941f49103031636bee078d
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,29 +1,38 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
github-authentication (
|
4
|
+
github-authentication (1.0.2)
|
5
5
|
jwt (~> 2.2)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
|
10
|
+
activesupport (6.1.4.1)
|
11
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
|
+
i18n (>= 1.6, < 2)
|
13
|
+
minitest (>= 5.1)
|
14
|
+
tzinfo (~> 2.0)
|
15
|
+
zeitwerk (~> 2.3)
|
16
|
+
addressable (2.8.0)
|
11
17
|
public_suffix (>= 2.0.2, < 5.0)
|
12
18
|
ast (2.4.0)
|
19
|
+
concurrent-ruby (1.1.9)
|
13
20
|
crack (0.4.3)
|
14
21
|
safe_yaml (~> 1.0.0)
|
15
22
|
hashdiff (1.0.1)
|
23
|
+
i18n (1.8.10)
|
24
|
+
concurrent-ruby (~> 1.0)
|
16
25
|
jaro_winkler (1.5.4)
|
17
|
-
jwt (2.
|
26
|
+
jwt (2.3.0)
|
18
27
|
minitest (5.14.0)
|
19
28
|
mocha (1.11.2)
|
20
29
|
parallel (1.19.1)
|
21
30
|
parser (2.7.1.2)
|
22
31
|
ast (~> 2.4.0)
|
23
|
-
public_suffix (4.0.
|
32
|
+
public_suffix (4.0.6)
|
24
33
|
rainbow (3.0.0)
|
25
34
|
rake (12.3.3)
|
26
|
-
rexml (3.2.
|
35
|
+
rexml (3.2.5)
|
27
36
|
rubocop (0.82.0)
|
28
37
|
jaro_winkler (~> 1.5.1)
|
29
38
|
parallel (~> 1.10)
|
@@ -37,18 +46,21 @@ GEM
|
|
37
46
|
ruby-progressbar (1.10.1)
|
38
47
|
safe_yaml (1.0.5)
|
39
48
|
timecop (0.9.1)
|
49
|
+
tzinfo (2.0.4)
|
50
|
+
concurrent-ruby (~> 1.0)
|
40
51
|
unicode-display_width (1.7.0)
|
41
52
|
vcr (5.1.0)
|
42
53
|
webmock (3.8.3)
|
43
54
|
addressable (>= 2.3.6)
|
44
55
|
crack (>= 0.3.2)
|
45
56
|
hashdiff (>= 0.4.0, < 2.0.0)
|
57
|
+
zeitwerk (2.4.2)
|
46
58
|
|
47
59
|
PLATFORMS
|
48
60
|
ruby
|
49
61
|
|
50
62
|
DEPENDENCIES
|
51
|
-
|
63
|
+
activesupport
|
52
64
|
github-authentication!
|
53
65
|
minitest (~> 5.0)
|
54
66
|
mocha (~> 1.11)
|
@@ -60,4 +72,4 @@ DEPENDENCIES
|
|
60
72
|
webmock (~> 3.8)
|
61
73
|
|
62
74
|
BUNDLED WITH
|
63
|
-
|
75
|
+
2.2.22
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# GithubAuthentication
|
2
2
|
|
3
3
|
This gem allows you to authenticate with GitHub. Specifically, as a [GitHub app](https://developer.github.com/apps/building-github-apps/creating-a-github-app/).
|
4
4
|
|
@@ -25,26 +25,27 @@ Or install it yourself as:
|
|
25
25
|
```ruby
|
26
26
|
require 'github-authentication'
|
27
27
|
|
28
|
-
cache =
|
29
|
-
generator =
|
28
|
+
cache = GithubAuthentication::Cache.new(storage: GithubAuthentication::ObjectCache.new)
|
29
|
+
generator = GithubAuthentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
|
30
30
|
installation_id: ENV['GITHUB_INSTALLATION_ID'],
|
31
31
|
app_id: ENV['GITHUB_APP_ID'])
|
32
|
-
provider =
|
32
|
+
provider = GithubAuthentication::Provider.new(generator: generator, cache: cache)
|
33
33
|
|
34
34
|
provider.token
|
35
|
+
provider.reset_token
|
35
36
|
```
|
36
37
|
|
37
38
|
### Cache
|
38
39
|
|
39
40
|
The cache takes a storage argument. You can pass an instance of an `ActiveSupport::Cache` implementation or use the provided
|
40
|
-
`
|
41
|
+
`GithubAuthentication::ObjectCache` if you are using it in a script.
|
41
42
|
|
42
43
|
### Generator::App
|
43
44
|
|
44
45
|
Generates a token for a GitHub app.
|
45
46
|
|
46
47
|
```ruby
|
47
|
-
|
48
|
+
GithubAuthentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
|
48
49
|
installation_id: ENV['GITHUB_INSTALLATION_ID'],
|
49
50
|
app_id: ENV['GITHUB_APP_ID'])
|
50
51
|
```
|
@@ -53,7 +54,7 @@ Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
|
|
53
54
|
|
54
55
|
Mostly for testing purposes you can provide a github token that gets retrieved.
|
55
56
|
```ruby
|
56
|
-
|
57
|
+
GithubAuthentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
|
57
58
|
```
|
58
59
|
|
59
60
|
## Example
|
@@ -70,16 +71,16 @@ module GitHub
|
|
70
71
|
def token
|
71
72
|
@token_provider ||= begin
|
72
73
|
if ENV['GITHUB_TOKEN']
|
73
|
-
storage =
|
74
|
-
generator =
|
74
|
+
storage = GithubAuthentication::ObjectCache.new
|
75
|
+
generator = GithubAuthentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
|
75
76
|
else
|
76
77
|
storage = ActiveSupport::Cache::RedisCacheStore.new
|
77
78
|
pem = Base64.decode64(ENV['GITHUB_PEM'])
|
78
|
-
generator =
|
79
|
+
generator = GithubAuthentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
|
79
80
|
app_id: APP_ID)
|
80
81
|
end
|
81
|
-
cache =
|
82
|
-
|
82
|
+
cache = GithubAuthentication::Cache.new(storage: storage)
|
83
|
+
GithubAuthentication::Provider.new(generator: generator, cache: cache)
|
83
84
|
end
|
84
85
|
@token_provider.token
|
85
86
|
end
|
@@ -95,6 +96,12 @@ module GitHub
|
|
95
96
|
end
|
96
97
|
```
|
97
98
|
|
99
|
+
## Git credential helper
|
100
|
+
|
101
|
+
This gem also ships with a [git credential helper][0] to authenticate git
|
102
|
+
operations as an App. See [this doc](docs/github_credential_helper.md) for
|
103
|
+
detail on setup and configuration.
|
104
|
+
|
98
105
|
## Development
|
99
106
|
|
100
107
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -109,3 +116,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
|
|
109
116
|
|
110
117
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
111
118
|
|
119
|
+
[0]: https://git-scm.com/docs/gitcredentials
|
data/bin/console
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
|
-
require "
|
5
|
+
require "github_authentication/provider"
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -0,0 +1,40 @@
|
|
1
|
+
## Git credential helper
|
2
|
+
|
3
|
+
The `github-authentication` gem bundles a [git credential helper][0] to
|
4
|
+
authenticate git operations as [GitHub Apps][1]. This is much preferred to
|
5
|
+
hard-coding long lived credentials like personal access tokens or SSH keys
|
6
|
+
inside e.g. a CI or build system.
|
7
|
+
|
8
|
+
For efficiency this helper requires the `activesupport` gem to allow for caching
|
9
|
+
credentials between git calls.
|
10
|
+
|
11
|
+
### Configuration
|
12
|
+
|
13
|
+
Token authentication is only supported over HTTPS, so as well as configuring the
|
14
|
+
credential helper we can also have git automatically rewrite SSH addresses. Add
|
15
|
+
this configuration snippet to `/etc/gitconfig` or `~/.gitconfig` depending on
|
16
|
+
your environment:
|
17
|
+
|
18
|
+
```
|
19
|
+
[url "https://github.com/"]
|
20
|
+
insteadOf = git@github.com:
|
21
|
+
insteadOf = ssh://git@github.com/
|
22
|
+
[credential "https://github.com"]
|
23
|
+
useHttpPath = true
|
24
|
+
helper = github-app
|
25
|
+
# Or, if using bundler:
|
26
|
+
# helper = !bundle exec git-credential-github-app
|
27
|
+
```
|
28
|
+
|
29
|
+
The credential helper also requires a few environment variables:
|
30
|
+
|
31
|
+
* `GITHUB_APP_ID` -> The App ID of the GitHub App
|
32
|
+
* `GITHUB_APP_INSTALLATION_ID` -> The Installation ID for the Org you want to
|
33
|
+
access
|
34
|
+
* `GITHUB_APP_KEYFILE` -> Path to a [private key][2] generated for your app
|
35
|
+
* `GITHUB_APP_CREDENTIAL_STORAGE_PATH` -> Directory to store cached credentials,
|
36
|
+
must already exist
|
37
|
+
|
38
|
+
[0]: https://git-scm.com/docs/gitcredentials
|
39
|
+
[1]: https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps
|
40
|
+
[2]: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#generating-a-private-key
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'github_authentication'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'active_support'
|
8
|
+
require 'active_support/cache'
|
9
|
+
require 'active_support/notifications'
|
10
|
+
rescue LoadError
|
11
|
+
warn("Active Support is required for the credential helper")
|
12
|
+
exit(2)
|
13
|
+
end
|
14
|
+
|
15
|
+
case ARGV[0]
|
16
|
+
when 'get'
|
17
|
+
exit_status = GithubAuthentication::GitCredentialHelper.new(
|
18
|
+
pem: File.read(ENV.fetch('GITHUB_APP_KEYFILE')),
|
19
|
+
app_id: ENV.fetch('GITHUB_APP_ID'),
|
20
|
+
installation_id: ENV.fetch('GITHUB_APP_INSTALLATION_ID'),
|
21
|
+
storage: ActiveSupport::Cache::FileStore.new(ENV.fetch('GITHUB_APP_CREDENTIAL_STORAGE_PATH'))
|
22
|
+
).handle_get
|
23
|
+
exit(exit_status)
|
24
|
+
when 'store'
|
25
|
+
# We maintain our own internal storage, so no-op any `store` requests
|
26
|
+
when nil, ''
|
27
|
+
warn('Supported operations: get, store')
|
28
|
+
exit(1)
|
29
|
+
else
|
30
|
+
warn("Unknown argument: #{ARGV[0]}")
|
31
|
+
exit(1)
|
32
|
+
end
|
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
lib = File.expand_path("../lib", __FILE__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require "
|
5
|
+
require "github_authentication/version"
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "github-authentication"
|
9
|
-
spec.version =
|
9
|
+
spec.version = GithubAuthentication::VERSION
|
10
10
|
spec.authors = ["Frederik Dudzik"]
|
11
11
|
spec.email = ["frederik.dudzik@shopify.com"]
|
12
12
|
|
13
|
-
spec.summary = "GitHub
|
13
|
+
spec.summary = "GitHub Authetication"
|
14
14
|
spec.description = "Authenticate with GitHub"
|
15
15
|
spec.homepage = "https://github.com/Shopify/github-authentication"
|
16
16
|
spec.license = "MIT"
|
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
if spec.respond_to?(:metadata)
|
21
21
|
spec.metadata["homepage_uri"] = spec.homepage
|
22
22
|
spec.metadata["source_code_uri"] = "https://github.com/Shopify/github-authentication"
|
23
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
23
24
|
else
|
24
25
|
raise "RubyGems 2.0 or newer is required to protect against " \
|
25
26
|
"public gem pushes."
|
@@ -36,7 +37,6 @@ Gem::Specification.new do |spec|
|
|
36
37
|
|
37
38
|
spec.add_dependency("jwt", "~> 2.2")
|
38
39
|
|
39
|
-
spec.add_development_dependency("bundler", "~> 1.17")
|
40
40
|
spec.add_development_dependency("rake", "~> 12.3")
|
41
41
|
spec.add_development_dependency("minitest", "~> 5.0")
|
42
42
|
spec.add_development_dependency("timecop", "~> 0.9")
|
@@ -45,4 +45,5 @@ Gem::Specification.new do |spec|
|
|
45
45
|
spec.add_development_dependency("vcr", "~> 5.1")
|
46
46
|
spec.add_development_dependency("rubocop", "~> 0.52")
|
47
47
|
spec.add_development_dependency("rubocop-shopify")
|
48
|
+
spec.add_development_dependency("activesupport")
|
48
49
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'github_authentication/token'
|
4
|
+
|
5
|
+
module GithubAuthentication
|
6
|
+
class Cache
|
7
|
+
# storage = ActiveSupport::Cache
|
8
|
+
def initialize(storage:, key: '')
|
9
|
+
@storage = storage
|
10
|
+
@key = "github:authentication:#{key}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
json = @storage.read(@key)
|
15
|
+
Token.from_json(json)
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(token)
|
19
|
+
@storage.write(@key, token.to_json, expires_in: token.expires_in)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
@storage.delete(@key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
require "uri"
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
require "github_authentication/http"
|
8
|
+
|
9
|
+
module GithubAuthentication
|
10
|
+
module Generator
|
11
|
+
class App
|
12
|
+
attr_reader :app_id, :installation_id
|
13
|
+
|
14
|
+
def initialize(pem:, installation_id:, app_id:)
|
15
|
+
@private_key = OpenSSL::PKey::RSA.new(pem)
|
16
|
+
@installation_id = installation_id
|
17
|
+
@app_id = app_id
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
url = "https://api.github.com/app/installations/#{installation_id}/access_tokens"
|
22
|
+
response = Http.post(url) do |request|
|
23
|
+
request["Authorization"] = "Bearer #{jwt}"
|
24
|
+
request["Accept"] = "application/vnd.github.machine-man-preview+json"
|
25
|
+
request
|
26
|
+
end
|
27
|
+
|
28
|
+
unless response.is_a?(Net::HTTPSuccess)
|
29
|
+
raise TokenGeneratorError, "[#{response.code}] #{response.body}"
|
30
|
+
end
|
31
|
+
|
32
|
+
Token.from_json(response.body)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def jwt
|
38
|
+
payload = {
|
39
|
+
# issued at time
|
40
|
+
iat: Time.now.utc.to_i,
|
41
|
+
# JWT expiration time (10 minute maximum)
|
42
|
+
exp: Time.now.utc.to_i + (10 * 60),
|
43
|
+
# GitHub App's identifier
|
44
|
+
iss: app_id,
|
45
|
+
}
|
46
|
+
JWT.encode(payload, @private_key, "RS256")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "github_authentication/token"
|
4
|
+
|
5
|
+
module GithubAuthentication
|
6
|
+
module Generator
|
7
|
+
class Personal
|
8
|
+
def initialize(github_token:)
|
9
|
+
@github_token = github_token
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
a_year_from_now = Time.now.utc + 31_556_952
|
14
|
+
Token.new(@github_token, a_year_from_now)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "github_authentication/generator/app"
|
4
|
+
require "github_authentication/generator/personal"
|
5
|
+
|
6
|
+
module GithubAuthentication
|
7
|
+
module Generator
|
8
|
+
Error = Class.new(StandardError)
|
9
|
+
TokenGeneratorError = Class.new(Error)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubAuthentication
|
4
|
+
class GitCredentialHelper
|
5
|
+
def initialize(pem:, installation_id:, app_id:, storage: nil, stdin: $stdin)
|
6
|
+
@pem = pem
|
7
|
+
@installation_id = installation_id
|
8
|
+
@app_id = app_id
|
9
|
+
@storage = storage
|
10
|
+
@stdin = stdin
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_get
|
14
|
+
description = parse_stdin
|
15
|
+
|
16
|
+
unless description['protocol'] == 'https' && description['host'] == 'github.com'
|
17
|
+
warn("Unsupported description: #{description}")
|
18
|
+
return 2
|
19
|
+
end
|
20
|
+
|
21
|
+
token = provider.token(seconds_ttl: min_cache_ttl)
|
22
|
+
puts("password=#{token}")
|
23
|
+
puts('username=api')
|
24
|
+
|
25
|
+
0
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def min_cache_ttl
|
31
|
+
# Tokens are valid for 60 minutes, allow a 10 minute buffer
|
32
|
+
10 * 60
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_stdin
|
36
|
+
# Credential description is written to STDIN in line delimited key=value form,
|
37
|
+
# see https://git-scm.com/docs/git-credential#IOFMT
|
38
|
+
@stdin.each_line.map { |line| line.split('=', 2).map(&:strip) }.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
def provider
|
42
|
+
@provider ||= Provider.new(
|
43
|
+
generator: generator,
|
44
|
+
cache: Cache.new(storage: @storage || ObjectCache.new)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def generator
|
49
|
+
@generator ||= Generator::App.new(
|
50
|
+
pem: @pem,
|
51
|
+
app_id: @app_id,
|
52
|
+
installation_id: @installation_id
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
require 'github_authentication/retriable'
|
7
|
+
|
8
|
+
module GithubAuthentication
|
9
|
+
module Http
|
10
|
+
class << self
|
11
|
+
include Retriable
|
12
|
+
|
13
|
+
def post(url)
|
14
|
+
uri = URI.parse(url)
|
15
|
+
with_retries(SystemCallError, Timeout::Error) do
|
16
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
17
|
+
http.use_ssl = true
|
18
|
+
http.start
|
19
|
+
begin
|
20
|
+
|
21
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
22
|
+
yield(request) if block_given?
|
23
|
+
|
24
|
+
http.request(request)
|
25
|
+
ensure
|
26
|
+
http.finish
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'github_authentication/token'
|
4
|
+
|
5
|
+
module GithubAuthentication
|
6
|
+
class ObjectCache
|
7
|
+
def initialize
|
8
|
+
@cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def read(key)
|
12
|
+
return unless @cache.key?(key)
|
13
|
+
|
14
|
+
options = @cache[key][:options]
|
15
|
+
if options.key?(:expires_at) && Time.now.utc > options[:expires_at]
|
16
|
+
@cache.delete(key)
|
17
|
+
return nil
|
18
|
+
end
|
19
|
+
|
20
|
+
@cache[key][:value]
|
21
|
+
end
|
22
|
+
|
23
|
+
def write(key, value, options = {})
|
24
|
+
if options.key?(:expires_in)
|
25
|
+
options[:expires_at] = Time.now.utc + options[:expires_in]
|
26
|
+
end
|
27
|
+
@cache[key] = { value: value, options: options }
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear(key)
|
31
|
+
@cache.delete(key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "mutex_m.rb"
|
3
|
+
|
4
|
+
require 'github_authentication/retriable'
|
5
|
+
|
6
|
+
module GithubAuthentication
|
7
|
+
class Provider
|
8
|
+
include Retriable
|
9
|
+
include Mutex_m
|
10
|
+
|
11
|
+
Error = Class.new(StandardError)
|
12
|
+
TokenGeneratorError = Class.new(Error)
|
13
|
+
|
14
|
+
def initialize(generator:, cache:)
|
15
|
+
super()
|
16
|
+
@token = nil
|
17
|
+
@generator = generator
|
18
|
+
@cache = cache
|
19
|
+
end
|
20
|
+
|
21
|
+
def token(seconds_ttl: 5 * 60)
|
22
|
+
return @token if @token&.valid_for?(seconds_ttl)
|
23
|
+
|
24
|
+
with_retries(TokenGeneratorError) do
|
25
|
+
mu_synchronize do
|
26
|
+
return @token if @token&.valid_for?(seconds_ttl)
|
27
|
+
|
28
|
+
if (@token = @cache.read)
|
29
|
+
return @token if @token.valid_for?(seconds_ttl)
|
30
|
+
end
|
31
|
+
|
32
|
+
if (@token = @generator.generate)
|
33
|
+
if @token.valid_for?(seconds_ttl)
|
34
|
+
@cache.write(@token)
|
35
|
+
return @token
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
raise TokenGeneratorError, "Couldn't create a token with a TTL of #{seconds_ttl}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset_token
|
45
|
+
@token = nil
|
46
|
+
@cache.clear
|
47
|
+
end
|
48
|
+
|
49
|
+
# prevent credential leak
|
50
|
+
def inspect
|
51
|
+
"#<#{self.class.name}>"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubAuthentication
|
4
|
+
module Retriable
|
5
|
+
def with_retries(*exceptions, max_attempts: 4, sleep_between_attempts: 0.1, exponential_backoff: true)
|
6
|
+
attempt = 1
|
7
|
+
previous_failure = nil
|
8
|
+
|
9
|
+
begin
|
10
|
+
return_value = yield(attempt, previous_failure)
|
11
|
+
rescue *exceptions => exception
|
12
|
+
raise unless attempt < max_attempts
|
13
|
+
|
14
|
+
sleep_after_attempt(
|
15
|
+
attempt: attempt,
|
16
|
+
base_sleep_time: sleep_between_attempts,
|
17
|
+
exponential_backoff: exponential_backoff
|
18
|
+
)
|
19
|
+
|
20
|
+
attempt += 1
|
21
|
+
previous_failure = exception
|
22
|
+
retry
|
23
|
+
else
|
24
|
+
return_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def sleep_after_attempt(attempt:, base_sleep_time:, exponential_backoff:)
|
31
|
+
return unless base_sleep_time > 0
|
32
|
+
|
33
|
+
time_to_sleep = if exponential_backoff
|
34
|
+
calculate_exponential_backoff(attempt: attempt, base_sleep_time: base_sleep_time)
|
35
|
+
else
|
36
|
+
base_sleep_time
|
37
|
+
end
|
38
|
+
|
39
|
+
Kernel.sleep(time_to_sleep)
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_exponential_backoff(attempt:, base_sleep_time:)
|
43
|
+
# Double the max sleep time for every attempt (exponential backoff).
|
44
|
+
# Randomize sleep time for more optimal request distribution.
|
45
|
+
lower_bound = Float(base_sleep_time)
|
46
|
+
upper_bound = lower_bound * (2 << (attempt - 2))
|
47
|
+
Kernel.rand(lower_bound..upper_bound)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module GithubAuthentication
|
6
|
+
class Token
|
7
|
+
attr_reader :expires_at
|
8
|
+
|
9
|
+
def self.from_json(data)
|
10
|
+
return nil if data.nil?
|
11
|
+
|
12
|
+
token, expires_at = JSON.parse(data).values_at('token', 'expires_at')
|
13
|
+
new(token, Time.iso8601(expires_at))
|
14
|
+
rescue JSON::ParserError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(token, expires_at)
|
19
|
+
@token = token
|
20
|
+
@expires_at = expires_at
|
21
|
+
end
|
22
|
+
|
23
|
+
def expires_in
|
24
|
+
(@expires_at - Time.now.utc).to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def expired?(seconds_ttl: 300)
|
28
|
+
@expires_at < Time.now.utc + seconds_ttl
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_for?(ttl)
|
32
|
+
!expired?(seconds_ttl: ttl)
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
# Truncating the token should be enough not to leak it in error messages etc
|
37
|
+
"#<#{self.class.name} @token=#{truncate(@token, 10)} @expires_at=#{@expires_at}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_json
|
41
|
+
JSON.dump(token: @token, expires_at: @expires_at.iso8601)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
@token
|
46
|
+
end
|
47
|
+
alias_method :to_str, :to_s
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def truncate(string, max)
|
52
|
+
string.length > max ? "#{string[0...max]}..." : string
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "github_authentication/version"
|
4
|
+
require "github_authentication/generator"
|
5
|
+
require "github_authentication/provider"
|
6
|
+
require "github_authentication/cache"
|
7
|
+
require "github_authentication/object_cache"
|
8
|
+
require "github_authentication/git_credential_helper"
|
9
|
+
|
10
|
+
module GithubAuthentication
|
11
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github-authentication
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frederik Dudzik
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.2'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.17'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.17'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: rake
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,10 +136,25 @@ dependencies:
|
|
150
136
|
- - ">="
|
151
137
|
- !ruby/object:Gem::Version
|
152
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: activesupport
|
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
153
|
description: Authenticate with GitHub
|
154
154
|
email:
|
155
155
|
- frederik.dudzik@shopify.com
|
156
|
-
executables:
|
156
|
+
executables:
|
157
|
+
- git-credential-github-app
|
157
158
|
extensions: []
|
158
159
|
extra_rdoc_files: []
|
159
160
|
files:
|
@@ -170,24 +171,29 @@ files:
|
|
170
171
|
- bin/rake
|
171
172
|
- bin/rubocop
|
172
173
|
- bin/setup
|
174
|
+
- docs/github_credential_helper.md
|
175
|
+
- exe/git-credential-github-app
|
173
176
|
- github-authentication.gemspec
|
174
|
-
- lib/github
|
175
|
-
- lib/
|
176
|
-
- lib/
|
177
|
-
- lib/
|
178
|
-
- lib/
|
179
|
-
- lib/
|
180
|
-
- lib/
|
181
|
-
- lib/
|
182
|
-
- lib/
|
183
|
-
- lib/
|
184
|
-
- lib/
|
177
|
+
- lib/github-authentication.rb
|
178
|
+
- lib/github_authentication.rb
|
179
|
+
- lib/github_authentication/cache.rb
|
180
|
+
- lib/github_authentication/generator.rb
|
181
|
+
- lib/github_authentication/generator/app.rb
|
182
|
+
- lib/github_authentication/generator/personal.rb
|
183
|
+
- lib/github_authentication/git_credential_helper.rb
|
184
|
+
- lib/github_authentication/http.rb
|
185
|
+
- lib/github_authentication/object_cache.rb
|
186
|
+
- lib/github_authentication/provider.rb
|
187
|
+
- lib/github_authentication/retriable.rb
|
188
|
+
- lib/github_authentication/token.rb
|
189
|
+
- lib/github_authentication/version.rb
|
185
190
|
homepage: https://github.com/Shopify/github-authentication
|
186
191
|
licenses:
|
187
192
|
- MIT
|
188
193
|
metadata:
|
189
194
|
homepage_uri: https://github.com/Shopify/github-authentication
|
190
195
|
source_code_uri: https://github.com/Shopify/github-authentication
|
196
|
+
allowed_push_host: https://rubygems.org
|
191
197
|
post_install_message:
|
192
198
|
rdoc_options: []
|
193
199
|
require_paths:
|
@@ -203,8 +209,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
209
|
- !ruby/object:Gem::Version
|
204
210
|
version: '0'
|
205
211
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
212
|
+
rubygems_version: 3.2.20
|
207
213
|
signing_key:
|
208
214
|
specification_version: 4
|
209
|
-
summary: GitHub
|
215
|
+
summary: GitHub Authetication
|
210
216
|
test_files: []
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'github/authentication/token'
|
4
|
-
|
5
|
-
module Github
|
6
|
-
module Authentication
|
7
|
-
class Cache
|
8
|
-
# storage = ActiveSupport::Cache
|
9
|
-
def initialize(storage:, key: '')
|
10
|
-
@storage = storage
|
11
|
-
@key = "github:authentication:#{key}"
|
12
|
-
end
|
13
|
-
|
14
|
-
def read
|
15
|
-
json = @storage.read(@key)
|
16
|
-
Token.from_json(json)
|
17
|
-
end
|
18
|
-
|
19
|
-
def write(token)
|
20
|
-
@storage.write(@key, token.to_json, expires_in: token.expires_in)
|
21
|
-
end
|
22
|
-
|
23
|
-
def clear
|
24
|
-
@storage.delete(@key)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'jwt'
|
4
|
-
require "uri"
|
5
|
-
require 'openssl'
|
6
|
-
|
7
|
-
require "github/authentication/http"
|
8
|
-
|
9
|
-
module Github
|
10
|
-
module Authentication
|
11
|
-
module Generator
|
12
|
-
class App
|
13
|
-
attr_reader :app_id, :installation_id
|
14
|
-
|
15
|
-
def initialize(pem:, installation_id:, app_id:)
|
16
|
-
@private_key = OpenSSL::PKey::RSA.new(pem)
|
17
|
-
@installation_id = installation_id
|
18
|
-
@app_id = app_id
|
19
|
-
end
|
20
|
-
|
21
|
-
def generate
|
22
|
-
url = "https://api.github.com/app/installations/#{installation_id}/access_tokens"
|
23
|
-
response = Http.post(url) do |request|
|
24
|
-
request["Authorization"] = "Bearer #{jwt}"
|
25
|
-
request["Accept"] = "application/vnd.github.machine-man-preview+json"
|
26
|
-
request
|
27
|
-
end
|
28
|
-
Token.from_json(response.body)
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def jwt
|
34
|
-
payload = {
|
35
|
-
# issued at time
|
36
|
-
iat: Time.now.utc.to_i,
|
37
|
-
# JWT expiration time (10 minute maximum)
|
38
|
-
exp: Time.now.utc.to_i + (10 * 60),
|
39
|
-
# GitHub App's identifier
|
40
|
-
iss: app_id,
|
41
|
-
}
|
42
|
-
JWT.encode(payload, @private_key, "RS256")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "github/authentication/token"
|
4
|
-
|
5
|
-
module Github
|
6
|
-
module Authentication
|
7
|
-
module Generator
|
8
|
-
class Personal
|
9
|
-
def initialize(github_token:)
|
10
|
-
@github_token = github_token
|
11
|
-
end
|
12
|
-
|
13
|
-
def generate
|
14
|
-
a_year_from_now = Time.now.utc + 31_556_952
|
15
|
-
Token.new(@github_token, a_year_from_now)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'net/http'
|
4
|
-
require 'timeout'
|
5
|
-
|
6
|
-
require 'github/authentication/retriable'
|
7
|
-
|
8
|
-
module Github
|
9
|
-
module Authentication
|
10
|
-
module Http
|
11
|
-
class << self
|
12
|
-
include Retriable
|
13
|
-
|
14
|
-
def post(url)
|
15
|
-
uri = URI.parse(url)
|
16
|
-
with_retries(SystemCallError, Timeout::Error) do
|
17
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
18
|
-
http.use_ssl = true
|
19
|
-
http.start
|
20
|
-
begin
|
21
|
-
|
22
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
23
|
-
yield(request) if block_given?
|
24
|
-
|
25
|
-
http.request(request)
|
26
|
-
ensure
|
27
|
-
http.finish
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'github/authentication/token'
|
4
|
-
|
5
|
-
module Github
|
6
|
-
module Authentication
|
7
|
-
class ObjectCache
|
8
|
-
def initialize
|
9
|
-
@cache = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def read(key)
|
13
|
-
return unless @cache.key?(key)
|
14
|
-
|
15
|
-
options = @cache[key][:options]
|
16
|
-
if options.key?(:expires_at) && Time.now.utc > options[:expires_at]
|
17
|
-
@cache.delete(key)
|
18
|
-
return nil
|
19
|
-
end
|
20
|
-
|
21
|
-
@cache[key][:value]
|
22
|
-
end
|
23
|
-
|
24
|
-
def write(key, value, options = {})
|
25
|
-
if options.key?(:expires_in)
|
26
|
-
options[:expires_at] = Time.now.utc + options[:expires_in] * 60
|
27
|
-
end
|
28
|
-
@cache[key] = { value: value, options: options }
|
29
|
-
end
|
30
|
-
|
31
|
-
def clear(key)
|
32
|
-
@cache.delete(key)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require "mutex_m.rb"
|
3
|
-
|
4
|
-
require 'github/authentication/retriable'
|
5
|
-
|
6
|
-
module Github
|
7
|
-
module Authentication
|
8
|
-
class Provider
|
9
|
-
include Retriable
|
10
|
-
include Mutex_m
|
11
|
-
|
12
|
-
Error = Class.new(StandardError)
|
13
|
-
TokenGeneratorError = Class.new(Error)
|
14
|
-
|
15
|
-
def initialize(generator:, cache:)
|
16
|
-
super()
|
17
|
-
@token = nil
|
18
|
-
@generator = generator
|
19
|
-
@cache = cache
|
20
|
-
end
|
21
|
-
|
22
|
-
def token(seconds_ttl: 5 * 60)
|
23
|
-
return @token if @token&.valid_for?(seconds_ttl)
|
24
|
-
|
25
|
-
with_retries(TokenGeneratorError) do
|
26
|
-
mu_synchronize do
|
27
|
-
return @token if @token&.valid_for?(seconds_ttl)
|
28
|
-
|
29
|
-
if (@token = @cache.read)
|
30
|
-
return @token if @token.valid_for?(seconds_ttl)
|
31
|
-
end
|
32
|
-
|
33
|
-
if (@token = @generator.generate)
|
34
|
-
if @token.valid_for?(seconds_ttl)
|
35
|
-
@cache.write(@token)
|
36
|
-
return @token
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
raise TokenGeneratorError, "Couldn't create a token with a TTL of #{seconds_ttl}"
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def reset_token
|
46
|
-
@token = nil
|
47
|
-
@cache.clear
|
48
|
-
end
|
49
|
-
|
50
|
-
# prevent credential leak
|
51
|
-
def inspect
|
52
|
-
"#<#{self.class.name}>"
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Github
|
4
|
-
module Authentication
|
5
|
-
module Retriable
|
6
|
-
def with_retries(*exceptions, max_attempts: 4, sleep_between_attempts: 0.1, exponential_backoff: true)
|
7
|
-
attempt = 1
|
8
|
-
previous_failure = nil
|
9
|
-
|
10
|
-
begin
|
11
|
-
return_value = yield(attempt, previous_failure)
|
12
|
-
rescue *exceptions => exception
|
13
|
-
raise unless attempt < max_attempts
|
14
|
-
|
15
|
-
sleep_after_attempt(
|
16
|
-
attempt: attempt,
|
17
|
-
base_sleep_time: sleep_between_attempts,
|
18
|
-
exponential_backoff: exponential_backoff
|
19
|
-
)
|
20
|
-
|
21
|
-
attempt += 1
|
22
|
-
previous_failure = exception
|
23
|
-
retry
|
24
|
-
else
|
25
|
-
return_value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def sleep_after_attempt(attempt:, base_sleep_time:, exponential_backoff:)
|
32
|
-
return unless base_sleep_time > 0
|
33
|
-
|
34
|
-
time_to_sleep = if exponential_backoff
|
35
|
-
calculate_exponential_backoff(attempt: attempt, base_sleep_time: base_sleep_time)
|
36
|
-
else
|
37
|
-
base_sleep_time
|
38
|
-
end
|
39
|
-
|
40
|
-
Kernel.sleep(time_to_sleep)
|
41
|
-
end
|
42
|
-
|
43
|
-
def calculate_exponential_backoff(attempt:, base_sleep_time:)
|
44
|
-
# Double the max sleep time for every attempt (exponential backoff).
|
45
|
-
# Randomize sleep time for more optimal request distribution.
|
46
|
-
lower_bound = Float(base_sleep_time)
|
47
|
-
upper_bound = lower_bound * (2 << (attempt - 2))
|
48
|
-
Kernel.rand(lower_bound..upper_bound)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
module Github
|
6
|
-
module Authentication
|
7
|
-
class Token
|
8
|
-
attr_reader :expires_at
|
9
|
-
|
10
|
-
def self.from_json(data)
|
11
|
-
return nil if data.nil?
|
12
|
-
|
13
|
-
token, expires_at = JSON.parse(data).values_at('token', 'expires_at')
|
14
|
-
new(token, Time.iso8601(expires_at))
|
15
|
-
rescue JSON::ParserError
|
16
|
-
nil
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize(token, expires_at)
|
20
|
-
@token = token
|
21
|
-
@expires_at = expires_at
|
22
|
-
end
|
23
|
-
|
24
|
-
def expires_in
|
25
|
-
(@expires_at - Time.now.utc).to_i / 60
|
26
|
-
end
|
27
|
-
|
28
|
-
def expired?(seconds_ttl: 300)
|
29
|
-
@expires_at < Time.now.utc + seconds_ttl
|
30
|
-
end
|
31
|
-
|
32
|
-
def valid_for?(ttl)
|
33
|
-
!expired?(seconds_ttl: ttl)
|
34
|
-
end
|
35
|
-
|
36
|
-
def inspect
|
37
|
-
# Truncating the token should be enough not to leak it in error messages etc
|
38
|
-
"#<#{self.class.name} @token=#{truncate(@token, 10)} @expires_at=#{@expires_at}>"
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_json
|
42
|
-
JSON.dump(token: @token, expires_at: @expires_at.iso8601)
|
43
|
-
end
|
44
|
-
|
45
|
-
def to_s
|
46
|
-
@token
|
47
|
-
end
|
48
|
-
alias_method :to_str, :to_s
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def truncate(string, max)
|
53
|
-
string.length > max ? "#{string[0...max]}..." : string
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|