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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10d63c1d1bf541971469409ba5a47e3ff2ca8e34d53300d4f0af5aa3d28aed22
4
- data.tar.gz: 12facda5d7b25f772a56732419d9f0e934d385a3e85451bd44b7ff8002fb0dff
3
+ metadata.gz: 60a26b87df203d0cad754620484922850747654443f26cc73ec60660f7b414f5
4
+ data.tar.gz: 959c06de8b2f7e4c7647baf651021c8c59071f655815cdb79b5958dd437e1628
5
5
  SHA512:
6
- metadata.gz: 31b569ecaebbbd1107ff9c9d0bad15baea88b230b3958a62f8a8e6c7af7d1d20c405a1d2d5008fcf12cf0e72faf5b861d158bca069181996b8ab23b84067f72d
7
- data.tar.gz: 45519a0abbaa8b7ce2ff5e6ca42be4a845d078bc5b1b6e4b8b57e4161c38bd41e52d3f0cb5a288b6e0058622dcc9ffc39971100598c70fa5a79b29558c8b8ae1
6
+ metadata.gz: 60cbe032c88f1b003524acbe8f2ff55ac2d96b1b2605d646e25165253fdfdd91f6e218a4c8ecd064ad0e9f7818a6dccdfa95c7752bc4db7b48294e6ca995c3c4
7
+ data.tar.gz: ca10a2668358b6c061e0d786aa8a5d637ca551ae5d65bf1640edce9064c939f71c391ae49fbba5f10fe46230c18b5f950de5c0a98b7b8a342bfff80714d80aa4
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.2)
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
- 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.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.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
 
@@ -23,13 +23,13 @@ Or install it yourself as:
23
23
  ## Usage
24
24
 
25
25
  ```ruby
26
- require 'github/authentication'
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
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
- `Github::Authentication::ObjectCache` if you are using it in a script.
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
- Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
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
- Github::Authentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
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 = Github::Authentication::ObjectCache.new
75
- 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'])
76
76
  else
77
77
  storage = ActiveSupport::Cache::RedisCacheStore.new
78
78
  pem = Base64.decode64(ENV['GITHUB_PEM'])
79
- generator = Github::Authentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
79
+ generator = GithubAuthentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
80
80
  app_id: APP_ID)
81
81
  end
82
- cache = Github::Authentication::Cache.new(storage: storage)
83
- Github::Authentication::Provider.new(generator: generator, cache: cache)
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 "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,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 "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,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.0"
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.2
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: 2020-08-17 00:00:00.000000000 Z
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/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/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.0.3
211
+ rubygems_version: 3.2.20
207
212
  signing_key:
208
213
  specification_version: 4
209
- summary: GitHub authetication
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,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]
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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Github
4
- module Authentication
5
- VERSION = "0.1.2"
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"