github-authentication 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +19 -7
- data/README.md +20 -13
- data/bin/console +1 -1
- data/docs/github_credential_helper.md +40 -0
- data/exe/git-credential-github-app +31 -0
- data/github-authentication.gemspec +5 -4
- 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 +35 -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: 60a26b87df203d0cad754620484922850747654443f26cc73ec60660f7b414f5
|
4
|
+
data.tar.gz: 959c06de8b2f7e4c7647baf651021c8c59071f655815cdb79b5958dd437e1628
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60cbe032c88f1b003524acbe8f2ff55ac2d96b1b2605d646e25165253fdfdd91f6e218a4c8ecd064ad0e9f7818a6dccdfa95c7752bc4db7b48294e6ca995c3c4
|
7
|
+
data.tar.gz: ca10a2668358b6c061e0d786aa8a5d637ca551ae5d65bf1640edce9064c939f71c391ae49fbba5f10fe46230c18b5f950de5c0a98b7b8a342bfff80714d80aa4
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,29 +1,38 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
github-authentication (0.
|
4
|
+
github-authentication (1.0.0)
|
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.2.
|
26
|
+
jwt (2.2.3)
|
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
|
|
@@ -23,13 +23,13 @@ Or install it yourself as:
|
|
23
23
|
## Usage
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
require '
|
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
35
|
provider.reset_token
|
@@ -38,14 +38,14 @@ provider.reset_token
|
|
38
38
|
### Cache
|
39
39
|
|
40
40
|
The cache takes a storage argument. You can pass an instance of an `ActiveSupport::Cache` implementation or use the provided
|
41
|
-
`
|
41
|
+
`GithubAuthentication::ObjectCache` if you are using it in a script.
|
42
42
|
|
43
43
|
### Generator::App
|
44
44
|
|
45
45
|
Generates a token for a GitHub app.
|
46
46
|
|
47
47
|
```ruby
|
48
|
-
|
48
|
+
GithubAuthentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
|
49
49
|
installation_id: ENV['GITHUB_INSTALLATION_ID'],
|
50
50
|
app_id: ENV['GITHUB_APP_ID'])
|
51
51
|
```
|
@@ -54,7 +54,7 @@ Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
|
|
54
54
|
|
55
55
|
Mostly for testing purposes you can provide a github token that gets retrieved.
|
56
56
|
```ruby
|
57
|
-
|
57
|
+
GithubAuthentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
|
58
58
|
```
|
59
59
|
|
60
60
|
## Example
|
@@ -71,16 +71,16 @@ module GitHub
|
|
71
71
|
def token
|
72
72
|
@token_provider ||= begin
|
73
73
|
if ENV['GITHUB_TOKEN']
|
74
|
-
storage =
|
75
|
-
generator =
|
74
|
+
storage = GithubAuthentication::ObjectCache.new
|
75
|
+
generator = GithubAuthentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
|
76
76
|
else
|
77
77
|
storage = ActiveSupport::Cache::RedisCacheStore.new
|
78
78
|
pem = Base64.decode64(ENV['GITHUB_PEM'])
|
79
|
-
generator =
|
79
|
+
generator = GithubAuthentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
|
80
80
|
app_id: APP_ID)
|
81
81
|
end
|
82
|
-
cache =
|
83
|
-
|
82
|
+
cache = GithubAuthentication::Cache.new(storage: storage)
|
83
|
+
GithubAuthentication::Provider.new(generator: generator, cache: cache)
|
84
84
|
end
|
85
85
|
@token_provider.token
|
86
86
|
end
|
@@ -96,6 +96,12 @@ module GitHub
|
|
96
96
|
end
|
97
97
|
```
|
98
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
|
+
|
99
105
|
## Development
|
100
106
|
|
101
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.
|
@@ -110,3 +116,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
|
|
110
116
|
|
111
117
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
112
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,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'github_authentication'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'active_support/cache'
|
8
|
+
require 'active_support/notifications'
|
9
|
+
rescue LoadError
|
10
|
+
warn("Active Support is required for the credential helper")
|
11
|
+
exit(2)
|
12
|
+
end
|
13
|
+
|
14
|
+
case ARGV[0]
|
15
|
+
when 'get'
|
16
|
+
exit_status = GithubAuthentication::GitCredentialHelper.new(
|
17
|
+
pem: File.read(ENV.fetch('GITHUB_APP_KEYFILE')),
|
18
|
+
app_id: ENV.fetch('GITHUB_APP_ID'),
|
19
|
+
installation_id: ENV.fetch('GITHUB_APP_INSTALLATION_ID'),
|
20
|
+
storage: ActiveSupport::Cache::FileStore.new(ENV.fetch('GITHUB_APP_CREDENTIAL_STORAGE_PATH'))
|
21
|
+
).handle_get
|
22
|
+
exit(exit_status)
|
23
|
+
when 'store'
|
24
|
+
# We maintain our own internal storage, so no-op any `store` requests
|
25
|
+
when nil, ''
|
26
|
+
warn('Supported operations: get, store')
|
27
|
+
exit(1)
|
28
|
+
else
|
29
|
+
warn("Unknown argument: #{ARGV[0]}")
|
30
|
+
exit(1)
|
31
|
+
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: 0.
|
4
|
+
version: 1.0.0
|
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-10-04 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,28 @@ 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/
|
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/cache.rb
|
179
|
+
- lib/github_authentication/generator.rb
|
180
|
+
- lib/github_authentication/generator/app.rb
|
181
|
+
- lib/github_authentication/generator/personal.rb
|
182
|
+
- lib/github_authentication/git_credential_helper.rb
|
183
|
+
- lib/github_authentication/http.rb
|
184
|
+
- lib/github_authentication/object_cache.rb
|
185
|
+
- lib/github_authentication/provider.rb
|
186
|
+
- lib/github_authentication/retriable.rb
|
187
|
+
- lib/github_authentication/token.rb
|
188
|
+
- lib/github_authentication/version.rb
|
185
189
|
homepage: https://github.com/Shopify/github-authentication
|
186
190
|
licenses:
|
187
191
|
- MIT
|
188
192
|
metadata:
|
189
193
|
homepage_uri: https://github.com/Shopify/github-authentication
|
190
194
|
source_code_uri: https://github.com/Shopify/github-authentication
|
195
|
+
allowed_push_host: https://rubygems.org
|
191
196
|
post_install_message:
|
192
197
|
rdoc_options: []
|
193
198
|
require_paths:
|
@@ -203,8 +208,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
208
|
- !ruby/object:Gem::Version
|
204
209
|
version: '0'
|
205
210
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
211
|
+
rubygems_version: 3.2.20
|
207
212
|
signing_key:
|
208
213
|
specification_version: 4
|
209
|
-
summary: GitHub
|
214
|
+
summary: GitHub Authetication
|
210
215
|
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]
|
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
|
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
|