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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +19 -7
  5. data/README.md +20 -12
  6. data/bin/console +1 -1
  7. data/docs/github_credential_helper.md +40 -0
  8. data/exe/git-credential-github-app +32 -0
  9. data/github-authentication.gemspec +5 -4
  10. data/lib/github-authentication.rb +3 -0
  11. data/lib/github_authentication/cache.rb +26 -0
  12. data/lib/github_authentication/generator/app.rb +50 -0
  13. data/lib/github_authentication/generator/personal.rb +18 -0
  14. data/lib/github_authentication/generator.rb +11 -0
  15. data/lib/github_authentication/git_credential_helper.rb +56 -0
  16. data/lib/github_authentication/http.rb +32 -0
  17. data/lib/github_authentication/object_cache.rb +34 -0
  18. data/lib/github_authentication/provider.rb +54 -0
  19. data/lib/github_authentication/retriable.rb +50 -0
  20. data/lib/github_authentication/token.rb +55 -0
  21. data/lib/github_authentication/version.rb +5 -0
  22. data/lib/github_authentication.rb +11 -0
  23. metadata +36 -30
  24. data/lib/github/authentication/cache.rb +0 -28
  25. data/lib/github/authentication/generator/app.rb +0 -47
  26. data/lib/github/authentication/generator/personal.rb +0 -20
  27. data/lib/github/authentication/generator.rb +0 -4
  28. data/lib/github/authentication/http.rb +0 -34
  29. data/lib/github/authentication/object_cache.rb +0 -36
  30. data/lib/github/authentication/provider.rb +0 -56
  31. data/lib/github/authentication/retriable.rb +0 -52
  32. data/lib/github/authentication/token.rb +0 -57
  33. data/lib/github/authentication/version.rb +0 -7
  34. data/lib/github/authentication.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13ed97d12d8f6c281dddeace6aa4f3f9cf89acec84329dfb40af0e9e47c79aa5
4
- data.tar.gz: 7fbc0d346197fa5ec32969f93ba9e021a6c3f259fc25bf5c5d06f47035efd477
3
+ metadata.gz: a279af637a4369feb5bff421697bed089b09f8096d6829eccb37b3c4911d7ba9
4
+ data.tar.gz: edb60c288c631aafd17fbc0b361fef03f86e019a38a77d1debcc1544627e424d
5
5
  SHA512:
6
- metadata.gz: 796745050e0f7b9712eb28c7498c1e88c4032ffb6d0eb0a6ca6aaa6522190541879e115dfb329ca1beed45e511902b0d2a8761cd1ce83113391070a21171142b
7
- data.tar.gz: 0c8ff23991ac13f648ad51bd108744cd4185a10bad1a13dc1e691d95d0c4d85d866d64bbf8d9f777b1bf92aa5da804600ad0751248825632d66a185a183ebc21
6
+ metadata.gz: 10259c8db9c441f9d899962653a0307cc0eed4584797135857c883a047c4609027909cf7bfdc2828ebe6b3b9f520244cdf4267e0fa8957fa2daea46795bbc83b
7
+ data.tar.gz: f6d4ab126dbc799ec03fea9a6d7c5893d4abae73fb50b16cb3a15169876f615ee01756593d21d1b86f7cadf88b8630faa6f7fd7ca7941f49103031636bee078d
data/.rubocop.yml CHANGED
@@ -18,3 +18,7 @@ Style/MethodCallWithArgsParentheses:
18
18
  - refute_nil
19
19
  - assert_kind_of
20
20
  - refute_kind_of
21
+
22
+ Naming/FileName:
23
+ Exclude:
24
+ - lib/github-authentication.rb
data/Gemfile CHANGED
@@ -3,5 +3,5 @@ source "https://rubygems.org"
3
3
 
4
4
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
5
 
6
- # Specify your gem's dependencies in github-authentication-provider.gemspec
6
+ # Specify your gem's dependencies in github-authentication.gemspec
7
7
  gemspec
data/Gemfile.lock CHANGED
@@ -1,29 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- github-authentication (0.1.0)
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
- addressable (2.7.0)
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.1)
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.4)
32
+ public_suffix (4.0.6)
24
33
  rainbow (3.0.0)
25
34
  rake (12.3.3)
26
- rexml (3.2.4)
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
- bundler (~> 1.17)
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
- 1.17.3
75
+ 2.2.22
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Github::Authentication
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 = Github::Authentication::Cache.new(storage: Github::Authentication::ObjectCache.new)
29
- generator = Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
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 = Github::Authentication::Provider.new(generator: generator, cache: cache)
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
- `Github::Authentication::ObjectCache` if you are using it in a script.
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
- Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
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
- Github::Authentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
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 = Github::Authentication::ObjectCache.new
74
- generator = Github::Authentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
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 = Github::Authentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
79
+ generator = GithubAuthentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
79
80
  app_id: APP_ID)
80
81
  end
81
- cache = Github::Authentication::Cache.new(storage: storage)
82
- Github::Authentication::Provider.new(generator: generator, cache: cache)
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 "github/authentication/provider"
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 "github/authentication/version"
5
+ require "github_authentication/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "github-authentication"
9
- spec.version = Github::Authentication::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 authetication"
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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "github_authentication"
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GithubAuthentication
4
+ VERSION = "1.0.2"
5
+ 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.1.0
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: 2020-05-12 00:00:00.000000000 Z
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/authentication.rb
175
- - lib/github/authentication/cache.rb
176
- - lib/github/authentication/generator.rb
177
- - lib/github/authentication/generator/app.rb
178
- - lib/github/authentication/generator/personal.rb
179
- - lib/github/authentication/http.rb
180
- - lib/github/authentication/object_cache.rb
181
- - lib/github/authentication/provider.rb
182
- - lib/github/authentication/retriable.rb
183
- - lib/github/authentication/token.rb
184
- - lib/github/authentication/version.rb
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.0.3
212
+ rubygems_version: 3.2.20
207
213
  signing_key:
208
214
  specification_version: 4
209
- summary: GitHub authetication
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,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "github/authentication/generator/app"
4
- require "github/authentication/generator/personal"
@@ -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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Github
4
- module Authentication
5
- VERSION = "0.1.0"
6
- end
7
- end
@@ -1,7 +0,0 @@
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"