diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb index 58fee0820..fc734676b 100644 --- a/app/models/concerns/authenticable.rb +++ b/app/models/concerns/authenticable.rb @@ -31,10 +31,40 @@ def encode_alb_token(payload) nil end - # decode JWT token using SHA-256 hash algorithm + # use only for testing, as we don't have private key for JWT encoded by Globus + def encode_globus_token(payload) + return nil if payload.blank? || !Rails.env.test? + + # replace newline characters with actual newlines + private_key = OpenSSL::PKey.read(File.read(Rails.root.join("spec", "fixtures", "certs", "ec512-private.pem").to_s)) + JWT.encode(payload, private_key, 'RS512') + rescue OpenSSL::PKey::ECError => e + Rails.logger.error e.inspect + " for " + payload.inspect + + nil + end + + # decode JWT token. Check whether it is a DataCite or Globus JWT via the JWT header + # DataCite uses RS256, Globus uses RS512 def decode_token(token) - public_key = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'].to_s.gsub('\n', "\n")) - payload = (JWT.decode token, public_key, true, { :algorithm => 'RS256' }).first + # check that JWT has header, payload and secret, separated by dot + token_parts = token.to_s.split(".") + raise JWT::DecodeError if token_parts.length != 3 + + # decode token + header = JSON.parse(Base64.urlsafe_decode64(token_parts.first)) + case header["alg"] + when "RS256" + # DataCite JWT + public_key = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'].to_s.gsub('\n', "\n")) + payload = (JWT.decode token, public_key, true, { :algorithm => 'RS256' }).first + when "RS512" + # Globus JWT + public_key = OpenSSL::PKey::RSA.new(cached_globus_public_key.fetch("n", nil).to_s.gsub('\n', "\n")) + payload = (JWT.decode token, public_key, true, { :algorithm => 'RS512' }).first + else + raise JWT::DecodeError, "Algorithm #{header["alg"]} is not supported." + end # check whether token has expired fail JWT::ExpiredSignature, "The token has expired." unless Time.now.to_i < payload["exp"].to_i @@ -191,6 +221,18 @@ def encode_alb_token(payload) nil end + # encode token using RSA and SHA-512 + # use this only for testing as private key is publicly available from ruby-jwt gem + def encode_globus_token(payload) + return nil if payload.blank? || !Rails.env.test? + private_key = OpenSSL::PKey.read(File.read(Rails.root.join("spec", "fixtures", "certs", "ec512-private.pem").to_s)) + JWT.encode(payload, private_key, 'RS512') + rescue OpenSSL::PKey::ECError => e + Rails.logger.error e.inspect + " for " + payload.inspect + + nil + end + # basic auth def encode_auth_param(username: nil, password: nil) return nil unless username.present? && password.present? diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb index d48f9cb1e..c872c7b3d 100644 --- a/app/models/concerns/cacheable.rb +++ b/app/models/concerns/cacheable.rb @@ -67,6 +67,14 @@ def cached_alb_public_key(kid) response.body.fetch("data", nil) end end + + def cached_globus_public_key + Rails.cache.fetch("globus_public_key", expires_in: 1.month) do + url = "https://auth.globus.org/jwk.json" + response = Maremma.get(url) + response.body.dig("data", "keys", 0) + end + end end module ClassMethods diff --git a/spec/concerns/authenticable_spec.rb b/spec/concerns/authenticable_spec.rb index d13aca379..b5fe758d7 100644 --- a/spec/concerns/authenticable_spec.rb +++ b/spec/concerns/authenticable_spec.rb @@ -4,7 +4,7 @@ let(:token) { User.generate_token } subject { User.new(token) } - describe 'decode_token' do + describe "decode_token DataCite" do it "has name" do payload = subject.decode_token(token) expect(payload["name"]).to eq("Josiah Carberry") @@ -28,6 +28,30 @@ end end + # describe "decode_token Globus", vcr: true do + # it "has name" do + # payload = subject.decode_token(token) + # expect(payload["name"]).to eq("Josiah Carberry") + # end + + # it "empty token" do + # payload = subject.decode_token("") + # expect(payload).to eq(errors: "The token could not be decoded.") + # end + + # it "invalid token" do + # payload = subject.decode_token("abc") + # expect(payload).to eq(errors: "The token could not be decoded.") + # end + + # it "expired token" do + # token = User.generate_token(exp: 0) + # subject = User.new(token) + # payload = subject.decode_token(token) + # expect(payload).to eq(errors: "The token has expired.") + # end + # end + describe 'decode_alb_token' do let(:token) { User.generate_alb_token } @@ -197,6 +221,18 @@ expect(token).to be_nil end end + + describe 'encode_globus_token' do + it "with name" do + token = subject.encode_globus_token("name" => "Josiah Carberry") + expect(token).to start_with("eyJhbG") + end + + it "empty string" do + token = subject.encode_globus_token("") + expect(token).to be_nil + end + end end describe Provider, type: :model do diff --git a/spec/fixtures/certs/ec512-private.pem b/spec/fixtures/certs/ec512-private.pem new file mode 100644 index 000000000..6b99b7f0d --- /dev/null +++ b/spec/fixtures/certs/ec512-private.pem @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG +zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw +aTbPY6TtWmxx6LJDzCX2nKWCPnKdZcEH9Ncu8g5RjRBRq2yacja3OoS6nA2YeDng +reBJxZr376P6Ns6XcQFWDA6K/MCTrEBCsPxXZNxd8KR9vMGWhgNtWRrcKzwJfQkr +suyehZkbbYyFnAWyARKHZuV7VUXmeEmRS/f93MPqVA== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-public.pem b/spec/fixtures/certs/ec512-public.pem new file mode 100644 index 000000000..3e5b266a4 --- /dev/null +++ b/spec/fixtures/certs/ec512-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBMO+GMGk2z2Ok7VpsceiyQ8wl9pyl +gj5ynWXBB/TXLvIOUY0QUatsmnI2tzqEupwNmHg54K3gScWa9++j+jbOl3EBVgwO +ivzAk6xAQrD8V2TcXfCkfbzBloYDbVka3Cs8CX0JK7LsnoWZG22MhZwFsgESh2bl +e1VF5nhJkUv3/dzD6lQ= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-wrong-private.pem b/spec/fixtures/certs/ec512-wrong-private.pem new file mode 100644 index 000000000..476fd3754 --- /dev/null +++ b/spec/fixtures/certs/ec512-wrong-private.pem @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEG/KbA2oCbiCT6L3V8XSz2WKBy0XhGvIFbl/ZkXIXnkYt+1B7wViSVo +KCHuMFsi6xU/5nE1EuDG2UsQJmKeAMkIOKAHBgUrgQQAI6GBiQOBhgAEAG0TFWe5 +cZ5DZIyfuysrCoQySTNxd+aT8sPIxsx7mW6YBTsuO6rEgxyegd2Auy4xtikxpzKv +soMXR02999Aaus2jAAt/wxrhhr41BDP4MV0b6Zngb72hna0pcGqit5OyU8AbOJUZ ++rdyowRGsOY+aPbOyVhdNcsEdxYC8GdIyCQLBC1H +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-wrong-public.pem b/spec/fixtures/certs/ec512-wrong-public.pem new file mode 100644 index 000000000..8ebeb0114 --- /dev/null +++ b/spec/fixtures/certs/ec512-wrong-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAbRMVZ7lxnkNkjJ+7KysKhDJJM3F3 +5pPyw8jGzHuZbpgFOy47qsSDHJ6B3YC7LjG2KTGnMq+ygxdHTb330Bq6zaMAC3/D +GuGGvjUEM/gxXRvpmeBvvaGdrSlwaqK3k7JTwBs4lRn6t3KjBEaw5j5o9s7JWF01 +ywR3FgLwZ0jIJAsELUc= +-----END PUBLIC KEY----- \ No newline at end of file