diff --git a/.rubocop.yml b/.rubocop.yml index 537f3da..5df1f71 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,3 +6,15 @@ Style/StringLiterals: Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +Metrics/BlockLength: + Max: 45 + +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent diff --git a/Gemfile b/Gemfile index f12b007..824db46 100644 --- a/Gemfile +++ b/Gemfile @@ -10,4 +10,5 @@ gem "rake", "~> 13.0" gem "minitest", "~> 5.16" +gem "debug" gem "rubocop", "~> 1.21" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..abdacba --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,83 @@ +PATH + remote: . + specs: + vauth (0.1.0) + jwt (~> 3.1, >= 3.1.2) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + base64 (0.3.0) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + erb (6.0.1) + io-console (0.8.2) + irb (1.16.0) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.18.0) + jwt (3.1.2) + base64 + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + minitest (5.27.0) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.7.0) + psych (5.3.1) + date + stringio + racc (1.8.1) + rainbow (3.1.1) + rake (13.3.1) + rdoc (7.0.3) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rubocop (1.82.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.48.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + stringio (3.2.0) + tsort (0.2.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + debug + irb + minitest (~> 5.16) + rake (~> 13.0) + rubocop (~> 1.21) + vauth! + +BUNDLED WITH + 2.6.7 diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4eb7d7b --- /dev/null +++ b/bin/rake @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rake", "rake") diff --git a/lib/vauth/auth_code_grant.rb b/lib/vauth/auth_code_grant.rb new file mode 100644 index 0000000..4d7f020 --- /dev/null +++ b/lib/vauth/auth_code_grant.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "json" +require "net/http" +require "vauth/identity_token" + +module Vauth + class AuthCodeGrant # :nodoc: + def initialize(client, code) + @client = client + @code = code + end + + def identity_token + ::Vauth::IdentityToken.new(parsed_response["id_token"]) + end + + private + + attr_reader :client, :code + + def parsed_response + @parsed_response = JSON.parse( + ::Net::HTTP.post_form(client.token_uri, { + client_id: client.id, + client_secret: client.secret, + scope: "openid", + code: code, + }).body + ) + end + end +end diff --git a/lib/vauth/client.rb b/lib/vauth/client.rb new file mode 100644 index 0000000..500b12e --- /dev/null +++ b/lib/vauth/client.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "uri" + +module Vauth + Client = Struct.new(:id, :secret, :token_uri) do + def token_uri + URI(self[:token_uri]) + end + end +end diff --git a/lib/vauth/identity_token.rb b/lib/vauth/identity_token.rb new file mode 100644 index 0000000..992d636 --- /dev/null +++ b/lib/vauth/identity_token.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Vauth + class IdentityToken # :nodoc: + def initialize(jwt_id_token) + @jwt_id_token = jwt_id_token + end + + def issuer + JWT.decode(jwt_id_token, nil, true, { algorithm: "none" })[0]["iss"] + end + + def subject + JWT.decode(jwt_id_token, nil, true, { algorithm: "none" })[0]["sub"] + end + + private + + attr_reader :jwt_id_token + end +end diff --git a/test/test_vauth.rb b/test/test_vauth.rb index 7184d9e..a5921c2 100644 --- a/test/test_vauth.rb +++ b/test/test_vauth.rb @@ -6,8 +6,4 @@ class TestVauth < Minitest::Test def test_that_it_has_a_version_number refute_nil ::Vauth::VERSION end - - def test_it_does_something_useful - assert false - end end diff --git a/test/vauth/auth_code_grant_test.rb b/test/vauth/auth_code_grant_test.rb new file mode 100644 index 0000000..68114d5 --- /dev/null +++ b/test/vauth/auth_code_grant_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "test_helper" +require "vauth/client" +require "vauth/auth_code_grant" +require "vauth/identity_token" + +describe ::Vauth::AuthCodeGrant do + subject { ::Vauth::AuthCodeGrant.new(client, code) } + + let(:client) { ::Vauth::Client.new(client_id, client_secret, token_uri) } + let(:code) { "code-for-auth-code-grant" } + let(:token_uri) { "https://oauth2-server.com/auth/token" } + let(:client_id) { "oauth2-server-given-client-id" } + let(:client_secret) { "oauth2-server-given-client-secret" } + let(:identity_token) { subject.identity_token } + + describe "#identity_token" do + it "returns the Identity Token with the issuer and the subject" do + response_mock = Minitest::Mock.new + response_mock.expect(:body, JSON.dump({ + id_token: JWT.encode({ + iss: "issuer-from-identity-token", + sub: "subject-from-identity-token", + }, nil, "none"), + })) + net_http_mock = Minitest::Mock.new + net_http_mock.expect(:call, response_mock, [ + URI(token_uri), + { + client_id: client_id, + client_secret: client_secret, + scope: "openid", + code: code, + } + ]) + + ::Net::HTTP.stub :post_form, net_http_mock do + _(identity_token).must_be_kind_of ::Vauth::IdentityToken + + _(identity_token.issuer).must_equal "issuer-from-identity-token" + _(identity_token.subject).must_equal "subject-from-identity-token" + end + + net_http_mock.verify + response_mock.verify + end + end +end diff --git a/test/vauth/client_test.rb b/test/vauth/client_test.rb new file mode 100644 index 0000000..fd19961 --- /dev/null +++ b/test/vauth/client_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "test_helper" +require "uri" + +require "vauth/client" + +describe ::Vauth::Client do + subject do + ::Vauth::Client.new( + "oauth2-server-given-client-id", + "oauth2-server-given-client-secret", + "https://oauth2-server.com/token" + ) + end + + describe "#token_uri" do + it "returns the passed Token URI" do + _(subject.token_uri).must_equal URI("https://oauth2-server.com/token") + end + end +end diff --git a/test/vauth/identity_token_test.rb b/test/vauth/identity_token_test.rb new file mode 100644 index 0000000..0e9ffc0 --- /dev/null +++ b/test/vauth/identity_token_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "test_helper" +require "jwt" + +require "vauth/identity_token" + +describe ::Vauth::IdentityToken do + let(:id_token_jwt) do + JWT.encode({ + iss: "some-issuer-from-oauth2-provider", + sub: "some-subject-from-oauth2-provider", + }, nil, "none") + end + + subject { ::Vauth::IdentityToken.new(id_token_jwt) } + + describe "#issuer" do + it 'returns the "iss" value from the decoded JWT' do + _(subject.issuer).must_equal "some-issuer-from-oauth2-provider" + end + end + + describe "#subject" do + it 'returns the "sub" value from the decoded JWT' do + _(subject.subject).must_equal "some-subject-from-oauth2-provider" + end + end +end diff --git a/vauth.gemspec b/vauth.gemspec index 5c0da5a..ab105ac 100644 --- a/vauth.gemspec +++ b/vauth.gemspec @@ -33,8 +33,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency "jwt", "~> 3.1", ">= 3.1.2" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html