moj-simple-jwt-auth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '069216a49c032609b1b2d4bb2b9fa9841efb7ff086d6eabec5f5f3394be7c6f7'
4
+ data.tar.gz: 6d6571c76a08ca15844b6e3251f900f53fcdcf981b90f3a5509835b80acf4306
5
+ SHA512:
6
+ metadata.gz: 1dfe076548da296f2233ae2b1103c01e5dad5940cbd6a2133b35258f41a24b36fe785681f940d191c57f3a1d92de96dbd968746a986f3e2a958e8150552644da
7
+ data.tar.gz: ff1c2a743a8f8d03c2a3f9d61cf8ba423e38fbd3b07847a85c7e78d5cd04c81c74615ee39baab1a3af233c87c87fb0651a9e95c873feb1336bbf4731d0aeb76e
@@ -0,0 +1,28 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ name: Testing with Ruby ${{ matrix.ruby }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ['3.0.4', '3.1.2']
17
+
18
+ steps:
19
+ - uses: actions/checkout@v3
20
+
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+
27
+ - name: Run the default task
28
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .rspec_status
11
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+ SuggestExtensions: false
4
+ NewCops: enable
5
+ Exclude:
6
+ - 'spec/**/*'
7
+ - 'vendor/**/*'
8
+ - 'vendor/bundle/**/*'
9
+
10
+ ####################################
11
+ ## Customization for this project ##
12
+ ####################################
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ Style/MultilineIfModifier:
18
+ Enabled: false
19
+
20
+ Metrics/AbcSize:
21
+ Max: 20
22
+
23
+ Metrics/MethodLength:
24
+ Max: 14
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'rspec'
8
+ gem 'rubocop'
9
+ gem 'simplecov'
10
+
11
+ # Transient dependencies, not part of the final gem
12
+ # Used for the middleware classes
13
+ gem 'faraday'
14
+ gem 'grape'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Ministry of Justice
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.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Simple JWT Auth ruby gem
2
+
3
+ A basic wrapper around [ruby-jwt gem](https://github.com/jwt/ruby-jwt) that includes two middlewares, one for Faraday
4
+ to facilitate generating the JWT token and sending it in the Authorization Bearer header, and another for Grape to facilitate
5
+ the verification of this token and handling of the payload in the rack env.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ # directly from Github
11
+ gem 'moj-simple-jwt-auth', github: 'ministryofjustice/moj-simple-jwt-auth'
12
+
13
+ # or from RubyGems
14
+ gem 'moj-simple-jwt-auth', '0.0.1'
15
+ ```
16
+
17
+ You can lock it to a specific version, branch or sha if you want too.
18
+
19
+ ## Usage
20
+
21
+ You need to configure the gem before you can use it. You can do this, for example with an initializer:
22
+
23
+ ```ruby
24
+ require 'simple_jwt_auth'
25
+
26
+ SimpleJwtAuth.configure do |config|
27
+ config.logger.level = Logger::DEBUG # default level is `info`
28
+ config.exp_seconds = 120 # default is 30 seconds
29
+ config.issuer = 'MyAppName' # only needed for consumers
30
+ config.secrets_config = { 'MyAppName' => 'qwerty' }
31
+ end
32
+ ````
33
+
34
+ For the **consumer** side, you usually only need to configure the `secrets_config` with one key (your issuer name) and one secret.
35
+ For the **producer** side (the API service for instance) you will need to configure a map of all allowed issuers with their corresponding secrets.
36
+
37
+ There are several options you can configure, like expiration, leeway, logging, and more. Please refer to the [Configuration class](lib/simple_jwt_auth/configuration.rb) for more details.
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ This gem uses rubocop and simplecov (at 100% coverage).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ministryofjustice/moj-simple-jwt-auth.
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ load 'tasks/rubocop.rake'
8
+
9
+ task(:default).prerequisites << :rubocop
10
+ task(:default).prerequisites << :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require_relative '../lib/simple_jwt_auth'
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__)
data/bin/setup ADDED
@@ -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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ class Claims
5
+ include Traits::Configurable
6
+
7
+ def initialize
8
+ @now = Time.now.to_i
9
+ end
10
+
11
+ def merge(other_hash)
12
+ to_h.merge(other_hash)
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ iat: @now,
18
+ exp: @now + config.exp_seconds,
19
+ iss: config.issuer
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ class Configuration
5
+ attr_accessor :logger,
6
+ :exp_seconds,
7
+ :exp_leeway,
8
+ :algorithm,
9
+ :issuer,
10
+ :secrets_config
11
+
12
+ def initialize
13
+ # Defaults to stdout and level info, can be configured
14
+ @logger = ::Logger.new($stdout)
15
+ @logger.level = ::Logger::INFO
16
+
17
+ # https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4
18
+ @exp_seconds = 30
19
+
20
+ # Small leeway to account for clock skew (seconds)
21
+ @exp_leeway = 10
22
+
23
+ # HMAC SHA-256 hash algorithm
24
+ @algorithm = 'HS256'
25
+
26
+ # A map of issuers and corresponding secrets
27
+ @secrets_config = {}
28
+ end
29
+ end
30
+
31
+ # Get current configuration
32
+ #
33
+ # @return [SimpleJwtAuth::Configuration] current configuration
34
+ #
35
+ def self.configuration
36
+ @configuration ||= Configuration.new
37
+ end
38
+
39
+ # Configure the gem
40
+ #
41
+ # Any attributes listed in +attr_accessor+ can be configured
42
+ #
43
+ # @example
44
+ # SimpleJwtAuth.configure do |config|
45
+ # config.issuer = 'MyApp'
46
+ # config.exp_seconds = 120
47
+ # end
48
+ #
49
+ def self.configure
50
+ yield configuration
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ class Decode
5
+ include Traits::Configurable
6
+
7
+ def initialize(token, options = {})
8
+ @token = token
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ JWT.decode(token, nil, true, options) do |_headers, payload|
14
+ issuer = payload['iss']
15
+
16
+ config.logger.debug "Decoding JWT token from issuer: #{issuer}"
17
+ secrets.secret_for(issuer)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :token
24
+
25
+ def options
26
+ @options.merge(
27
+ required_claims: %w[iss iat exp],
28
+ algorithm: config.algorithm,
29
+ exp_leeway: config.exp_leeway,
30
+ nbf_leeway: config.exp_leeway,
31
+ iss: secrets.issuers,
32
+ verify_iss: true,
33
+ verify_iat: true
34
+ )
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ class Encode
5
+ include Traits::Configurable
6
+
7
+ def initialize(payload: {}, claims: {}, header_fields: {})
8
+ @payload = payload
9
+ @claims = claims
10
+ @header_fields = header_fields
11
+ end
12
+
13
+ def call
14
+ jwt_payload = payload.merge(claims)
15
+ issuer = jwt_payload.fetch(:iss)
16
+
17
+ config.logger.debug "Encoding JWT payload: #{jwt_payload}"
18
+
19
+ JWT.encode(
20
+ jwt_payload,
21
+ secrets.secret_for(issuer),
22
+ config.algorithm,
23
+ header_fields
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :payload, :header_fields
30
+
31
+ def claims
32
+ Claims.new.merge(@claims)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ module Errors
5
+ class UndefinedIssuer < StandardError; end
6
+ class UnknownIssuer < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ module Middleware
5
+ module Faraday
6
+ class Jwt < ::Faraday::Middleware
7
+ AUTH_HEADER = 'Authorization'
8
+ AUTH_SCHEME = 'Bearer'
9
+
10
+ def initialize(app = nil, **kwargs)
11
+ @kwargs = kwargs
12
+ super(app)
13
+ end
14
+
15
+ def on_request(env)
16
+ env.request_headers[AUTH_HEADER] = [AUTH_SCHEME, jwt_token].join(' ')
17
+ end
18
+
19
+ private
20
+
21
+ def jwt_token
22
+ SimpleJwtAuth::Encode.new(**@kwargs).call
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # :nocov:
30
+ Faraday::Request.register_middleware(
31
+ jwt: SimpleJwtAuth::Middleware::Faraday::Jwt
32
+ )
33
+ # :nocov:
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ module Middleware
5
+ module Grape
6
+ class Jwt < ::Grape::Middleware::Auth::Base
7
+ ENV_AUTH_KEY = 'HTTP_AUTHORIZATION'
8
+ ENV_PAYLOAD_KEY = 'grape_jwt.payload'
9
+
10
+ def call(env)
11
+ return app.call(env) if test_env?(env)
12
+
13
+ token = env.fetch(ENV_AUTH_KEY, '').split.last
14
+
15
+ begin
16
+ payload, _header = SimpleJwtAuth::Decode.new(token).call
17
+ env[ENV_PAYLOAD_KEY] = payload
18
+
19
+ logger.debug "Authorized request, JWT payload: #{payload}"
20
+
21
+ app.call(env)
22
+ rescue JWT::DecodeError => e
23
+ logger.warn "Unauthorized request, JWT error: #{e.message}"
24
+
25
+ [401, { 'Content-Type' => 'application/json' }, [{ status: 401, error: e.message }.to_json]]
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def test_env?(env)
32
+ env['rack.test'] == true
33
+ end
34
+
35
+ def logger
36
+ SimpleJwtAuth.configuration.logger
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # :nocov:
44
+ Grape::Middleware::Auth::Strategies.add(
45
+ :jwt, SimpleJwtAuth::Middleware::Grape::Jwt, ->(options) { [options] }
46
+ )
47
+ # :nocov:
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ class Secrets
5
+ def initialize(secrets)
6
+ @secrets = secrets
7
+ end
8
+
9
+ # Get all configured issuers
10
+ #
11
+ # @return [Array] list of configured issuers
12
+ #
13
+ def issuers
14
+ secrets.keys
15
+ end
16
+
17
+ # Get secret for a specific issuer
18
+ # Used when decoding, to retrieve the secret for an issuer
19
+ #
20
+ # @param issuer [String] The issuer to retrieve their secret
21
+ #
22
+ # @raise [SimpleJwtAuth::Errors::UndefinedIssuer]
23
+ # @raise [SimpleJwtAuth::Errors::UnknownIssuer]
24
+ #
25
+ # @return [String] issuer secret
26
+ #
27
+ def secret_for(issuer)
28
+ raise(
29
+ Errors::UndefinedIssuer, 'issuer has not been configured'
30
+ ) unless issuer
31
+
32
+ raise(
33
+ Errors::UnknownIssuer, "issuer `#{issuer}` is not recognized"
34
+ ) unless secrets.key?(issuer)
35
+
36
+ secrets.fetch(issuer)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :secrets
42
+ end
43
+
44
+ # Get current secrets
45
+ #
46
+ # @return [SimpleJwtAuth::Secrets] current secrets
47
+ #
48
+ def self.secrets
49
+ @secrets ||= Secrets.new(configuration.secrets_config)
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ module Traits
5
+ module Configurable
6
+ private
7
+
8
+ def config
9
+ SimpleJwtAuth.configuration
10
+ end
11
+
12
+ def secrets
13
+ SimpleJwtAuth.secrets
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleJwtAuth
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'logger'
5
+
6
+ require_relative 'simple_jwt_auth/version'
7
+ require_relative 'simple_jwt_auth/configuration'
8
+ require_relative 'simple_jwt_auth/secrets'
9
+ require_relative 'simple_jwt_auth/errors'
10
+
11
+ require_relative 'simple_jwt_auth/traits/configurable'
12
+
13
+ require_relative 'simple_jwt_auth/claims'
14
+ require_relative 'simple_jwt_auth/encode'
15
+ require_relative 'simple_jwt_auth/decode'
16
+
17
+ # Middleware helpers
18
+ require_relative 'simple_jwt_auth/middleware/faraday/jwt' if defined?(Faraday)
19
+ require_relative 'simple_jwt_auth/middleware/grape/jwt' if defined?(Grape)
20
+
21
+ module SimpleJwtAuth
22
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Gem.loaded_specs.key?('rubocop')
4
+ require 'rubocop/rake_task'
5
+ RuboCop::RakeTask.new
6
+
7
+ task(:default).prerequisites << task(:rubocop)
8
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'simple_jwt_auth/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'moj-simple-jwt-auth'
9
+ spec.version = SimpleJwtAuth::VERSION
10
+
11
+ spec.authors = ['Jesus Laiz']
12
+ spec.email = ['zheileman@users.noreply.github.com']
13
+
14
+ spec.summary = 'Simple JWT Auth ruby gem with middleware for Faraday and Grape.'
15
+ spec.homepage = 'https://github.com/ministryofjustice/moj-simple-jwt-auth'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata = {
19
+ 'rubygems_mfa_required' => 'true'
20
+ }
21
+
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+ spec.required_ruby_version = '>= 3.0.0'
30
+
31
+ spec.add_dependency 'json'
32
+ spec.add_dependency 'jwt'
33
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: moj-simple-jwt-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jesus Laiz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - zheileman@users.noreply.github.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".github/workflows/main.yml"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - ".rubocop.yml"
52
+ - Gemfile
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - bin/console
57
+ - bin/setup
58
+ - lib/simple_jwt_auth.rb
59
+ - lib/simple_jwt_auth/claims.rb
60
+ - lib/simple_jwt_auth/configuration.rb
61
+ - lib/simple_jwt_auth/decode.rb
62
+ - lib/simple_jwt_auth/encode.rb
63
+ - lib/simple_jwt_auth/errors.rb
64
+ - lib/simple_jwt_auth/middleware/faraday/jwt.rb
65
+ - lib/simple_jwt_auth/middleware/grape/jwt.rb
66
+ - lib/simple_jwt_auth/secrets.rb
67
+ - lib/simple_jwt_auth/traits/configurable.rb
68
+ - lib/simple_jwt_auth/version.rb
69
+ - lib/tasks/rubocop.rake
70
+ - simple-jwt-auth.gemspec
71
+ homepage: https://github.com/ministryofjustice/moj-simple-jwt-auth
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ rubygems_mfa_required: 'true'
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 3.0.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.3.7
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Simple JWT Auth ruby gem with middleware for Faraday and Grape.
95
+ test_files: []