From 214bf12e07d8790910d4eb06aeb6f54c5d462552 Mon Sep 17 00:00:00 2001 From: Nipun Paradkar Date: Sun, 18 Jan 2026 01:40:34 +0530 Subject: [PATCH] Fetch ID Token via Authorization Code Grant Added the ability to fetch the ID Token via the Authorization Code Grant. The class responsible for doing the fetching is `::Vauth::AuthCodeGrant`. It returns a `::Vauth::IdentityToken` via its `#identity_token` method. `::Vauth::AuthCodeGrant` currently only has the ability to deal with the ID Token, as I'm only focusing on Open ID Connect for now. In the future, it should also have a method to fetch the Access Token. The `::Vauth::Client` struct holds the information that allows the grant object to make the request. Currently, I'm not sure if there's a better way to encapsulate the information handled via the `::Vauth::Client` class, but its refactor is on my mind if I find a better way. Other notable changes are related to RuboCop. I didn't like the defaults, so I changed it to my liking. The end goal is to collect all these and extract a gem out of it. Installed the `debug` gem as well for obvious reasons. It also created a `Gemfile.lock` that wasn't present earlier for some reason. Installed the `jwt` gem as well to decode the ID Token received from the OAuth2 Provider. Generated the binstub for Rake to make it easier to run the tests as well as RuboCop. Also had to remove a spec that was failing. It was just an example spec. --- .rubocop.yml | 12 +++++ Gemfile | 1 + Gemfile.lock | 83 ++++++++++++++++++++++++++++++ bin/rake | 27 ++++++++++ lib/vauth/auth_code_grant.rb | 33 ++++++++++++ lib/vauth/client.rb | 11 ++++ lib/vauth/identity_token.rb | 21 ++++++++ test/test_vauth.rb | 4 -- test/vauth/auth_code_grant_test.rb | 49 ++++++++++++++++++ test/vauth/client_test.rb | 22 ++++++++ test/vauth/identity_token_test.rb | 29 +++++++++++ vauth.gemspec | 3 +- 12 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 Gemfile.lock create mode 100755 bin/rake create mode 100644 lib/vauth/auth_code_grant.rb create mode 100644 lib/vauth/client.rb create mode 100644 lib/vauth/identity_token.rb create mode 100644 test/vauth/auth_code_grant_test.rb create mode 100644 test/vauth/client_test.rb create mode 100644 test/vauth/identity_token_test.rb 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