github-authentication 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13ed97d12d8f6c281dddeace6aa4f3f9cf89acec84329dfb40af0e9e47c79aa5
4
+ data.tar.gz: 7fbc0d346197fa5ec32969f93ba9e021a6c3f259fc25bf5c5d06f47035efd477
5
+ SHA512:
6
+ metadata.gz: 796745050e0f7b9712eb28c7498c1e88c4032ffb6d0eb0a6ca6aaa6522190541879e115dfb329ca1beed45e511902b0d2a8761cd1ce83113391070a21171142b
7
+ data.tar.gz: 0c8ff23991ac13f648ad51bd108744cd4185a10bad1a13dc1e691d95d0c4d85d866d64bbf8d9f777b1bf92aa5da804600ad0751248825632d66a185a183ebc21
@@ -0,0 +1,33 @@
1
+ name: Tests
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ ruby-tests:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ matrix:
11
+ ruby: [ '2.6' ]
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ - name: Set up Ruby ${{ matrix.ruby }}
15
+ uses: actions/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ - name: Setup timezone
19
+ uses: zcong1993/setup-timezone@master
20
+ with:
21
+ timezone: UTC
22
+ - name: Lint Ruby
23
+ run: |
24
+ gem install bundler
25
+ bundle install --jobs 4 --retry 3
26
+ bin/rubocop
27
+ - name: Run Ruby tests
28
+ run: |
29
+ gem install bundler
30
+ bundle install --jobs 4 --retry 3
31
+ bin/rake test
32
+ env:
33
+ SUITE: ruby
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,20 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.6
6
+ CacheRootDirectory: tmp/rubocop
7
+ UseCache: true
8
+
9
+ Style/MethodCallWithArgsParentheses:
10
+ IgnoredMethods:
11
+ - require
12
+ - raise
13
+ - assert
14
+ - refute
15
+ - assert_equal
16
+ - refute_equal
17
+ - assert_nil
18
+ - refute_nil
19
+ - assert_kind_of
20
+ - refute_kind_of
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.3
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ source "https://rubygems.org"
3
+
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+
6
+ # Specify your gem's dependencies in github-authentication-provider.gemspec
7
+ gemspec
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ github-authentication (0.1.0)
5
+ jwt (~> 2.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.7.0)
11
+ public_suffix (>= 2.0.2, < 5.0)
12
+ ast (2.4.0)
13
+ crack (0.4.3)
14
+ safe_yaml (~> 1.0.0)
15
+ hashdiff (1.0.1)
16
+ jaro_winkler (1.5.4)
17
+ jwt (2.2.1)
18
+ minitest (5.14.0)
19
+ mocha (1.11.2)
20
+ parallel (1.19.1)
21
+ parser (2.7.1.2)
22
+ ast (~> 2.4.0)
23
+ public_suffix (4.0.4)
24
+ rainbow (3.0.0)
25
+ rake (12.3.3)
26
+ rexml (3.2.4)
27
+ rubocop (0.82.0)
28
+ jaro_winkler (~> 1.5.1)
29
+ parallel (~> 1.10)
30
+ parser (>= 2.7.0.1)
31
+ rainbow (>= 2.2.2, < 4.0)
32
+ rexml
33
+ ruby-progressbar (~> 1.7)
34
+ unicode-display_width (>= 1.4.0, < 2.0)
35
+ rubocop-shopify (1.0.2)
36
+ rubocop (~> 0.82.0)
37
+ ruby-progressbar (1.10.1)
38
+ safe_yaml (1.0.5)
39
+ timecop (0.9.1)
40
+ unicode-display_width (1.7.0)
41
+ vcr (5.1.0)
42
+ webmock (3.8.3)
43
+ addressable (>= 2.3.6)
44
+ crack (>= 0.3.2)
45
+ hashdiff (>= 0.4.0, < 2.0.0)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ bundler (~> 1.17)
52
+ github-authentication!
53
+ minitest (~> 5.0)
54
+ mocha (~> 1.11)
55
+ rake (~> 12.3)
56
+ rubocop (~> 0.52)
57
+ rubocop-shopify
58
+ timecop (~> 0.9)
59
+ vcr (~> 5.1)
60
+ webmock (~> 3.8)
61
+
62
+ BUNDLED WITH
63
+ 1.17.3
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Frederik Dudzik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,111 @@
1
+ # Github::Authentication
2
+
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
+
5
+ The app works well with the ActiveSupport::Cache, uses retries to mitigate GitHub flakiness, and is thread safe
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'github-authentication'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install github-authentication
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require 'github-authentication'
27
+
28
+ cache = Github::Authentication::Cache.new(storage: Github::Authentication::ObjectCache.new)
29
+ generator = Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
30
+ installation_id: ENV['GITHUB_INSTALLATION_ID'],
31
+ app_id: ENV['GITHUB_APP_ID'])
32
+ provider = Github::Authentication::Provider.new(generator: generator, cache: cache)
33
+
34
+ provider.token
35
+ ```
36
+
37
+ ### Cache
38
+
39
+ 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
+
42
+ ### Generator::App
43
+
44
+ Generates a token for a GitHub app.
45
+
46
+ ```ruby
47
+ Github::Authentication::Generator::App.new(pem: ENV['GITHUB_PEM'],
48
+ installation_id: ENV['GITHUB_INSTALLATION_ID'],
49
+ app_id: ENV['GITHUB_APP_ID'])
50
+ ```
51
+
52
+ ### Generator::Personal
53
+
54
+ Mostly for testing purposes you can provide a github token that gets retrieved.
55
+ ```ruby
56
+ Github::Authentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
57
+ ```
58
+
59
+ ## Example
60
+
61
+ ```ruby
62
+
63
+ require "base64"
64
+
65
+ module GitHub
66
+ APP_ID = "<APP_ID>"
67
+ INSTALLATION_ID = "<INSTALLATION_ID>"
68
+
69
+ class << self
70
+ def token
71
+ @token_provider ||= begin
72
+ if ENV['GITHUB_TOKEN']
73
+ storage = Github::Authentication::ObjectCache.new
74
+ generator = Github::Authentication::Generator::Personal.new(github_token: ENV['GITHUB_TOKEN'])
75
+ else
76
+ storage = ActiveSupport::Cache::RedisCacheStore.new
77
+ pem = Base64.decode64(ENV['GITHUB_PEM'])
78
+ generator = Github::Authentication::Generator::App.new(pem: pem, installation_id: INSTALLATION_ID,
79
+ app_id: APP_ID)
80
+ end
81
+ cache = Github::Authentication::Cache.new(storage: storage)
82
+ Github::Authentication::Provider.new(generator: generator, cache: cache)
83
+ end
84
+ @token_provider.token
85
+ end
86
+
87
+ def client
88
+ if ENV['GITHUB_TOKEN']
89
+ Octokit::Client.new(access_token: token.to_s)
90
+ else
91
+ Octokit::Client.new(bearer_token: token.to_s)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Development
99
+
100
+ 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.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/github-authentication.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
111
+
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task(default: :test)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "github/authentication/provider"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load(Gem.bin_path("rake", "rake"))
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load(Gem.bin_path("rubocop", "rubocop"))
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "github/authentication/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "github-authentication"
9
+ spec.version = Github::Authentication::VERSION
10
+ spec.authors = ["Frederik Dudzik"]
11
+ spec.email = ["frederik.dudzik@shopify.com"]
12
+
13
+ spec.summary = "GitHub authetication"
14
+ spec.description = "Authenticate with GitHub"
15
+ spec.homepage = "https://github.com/Shopify/github-authentication"
16
+ spec.license = "MIT"
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/Shopify/github-authentication"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
31
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency("jwt", "~> 2.2")
38
+
39
+ spec.add_development_dependency("bundler", "~> 1.17")
40
+ spec.add_development_dependency("rake", "~> 12.3")
41
+ spec.add_development_dependency("minitest", "~> 5.0")
42
+ spec.add_development_dependency("timecop", "~> 0.9")
43
+ spec.add_development_dependency("mocha", "~> 1.11")
44
+ spec.add_development_dependency("webmock", "~> 3.8")
45
+ spec.add_development_dependency("vcr", "~> 5.1")
46
+ spec.add_development_dependency("rubocop", "~> 0.52")
47
+ spec.add_development_dependency("rubocop-shopify")
48
+ end
@@ -0,0 +1,7 @@
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"
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "github/authentication/generator/app"
4
+ require "github/authentication/generator/personal"
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,57 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Github
4
+ module Authentication
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github-authentication
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Frederik Dudzik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.11'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.11'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.8'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.1'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.1'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.52'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.52'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-shopify
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
+ description: Authenticate with GitHub
154
+ email:
155
+ - frederik.dudzik@shopify.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".github/workflows/tests.yml"
161
+ - ".gitignore"
162
+ - ".rubocop.yml"
163
+ - ".travis.yml"
164
+ - Gemfile
165
+ - Gemfile.lock
166
+ - LICENSE.txt
167
+ - README.md
168
+ - Rakefile
169
+ - bin/console
170
+ - bin/rake
171
+ - bin/rubocop
172
+ - bin/setup
173
+ - 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
185
+ homepage: https://github.com/Shopify/github-authentication
186
+ licenses:
187
+ - MIT
188
+ metadata:
189
+ homepage_uri: https://github.com/Shopify/github-authentication
190
+ source_code_uri: https://github.com/Shopify/github-authentication
191
+ post_install_message:
192
+ rdoc_options: []
193
+ require_paths:
194
+ - lib
195
+ required_ruby_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ requirements: []
206
+ rubygems_version: 3.0.3
207
+ signing_key:
208
+ specification_version: 4
209
+ summary: GitHub authetication
210
+ test_files: []