Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ gem 'ipaddress'
gem 'jaro_winkler', '~> 1.5.5'
gem 'jquery-rails'
gem 'jquery-ui-rails'
gem 'jwt', '~> 1.5', '>= 1.5.4'
gem 'jwt', '~> 2.5'
gem 'lograge', '>=0.11.2'
gem 'mutex_m' # Deprecation warning.
gem 'netaddr', '~> 1.5', '>= 1.5.1'
Expand Down
5 changes: 3 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ GEM
json (2.18.1)
jsonpath (0.5.8)
multi_json
jwt (1.5.6)
jwt (2.10.2)
base64
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
listen (3.10.0)
Expand Down Expand Up @@ -542,7 +543,7 @@ DEPENDENCIES
jaro_winkler (~> 1.5.5)
jquery-rails
jquery-ui-rails
jwt (~> 1.5, >= 1.5.4)
jwt (~> 2.5)
listen (~> 3.2)
lograge (>= 0.11.2)
mutex_m
Expand Down
49 changes: 49 additions & 0 deletions app/controllers/concerns/alma_jwt_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'jwt'
require 'net/http'
require 'json'

module AlmaJwtValidator
JWKS_URL = 'https://api-na.hosted.exlibrisgroup.com/auth/01UCS_BER/jwks.json'.freeze
EXPECTED_ISS = 'https://api-na.hosted.exlibrisgroup.com/auth/01UCS_BER'.freeze

module_function

def jwk_set
Rails.cache.fetch('jwks_set', expires_in: 4.hour) do
jwks_raw = Net::HTTP.get(URI(JWKS_URL))
jwks_keys = JSON.parse(jwks_raw)['keys']
JWT::JWK::Set.new(jwks_keys)
end
end

# rubocop:disable Metrics/MethodLength
def decode_and_verify_jwt(token)
# Decode header to get the 'kid'
header = JWT.decode(token, nil, false).last
kid = header['kid']

# Find the key from the JWK set
jwk = jwk_set.keys.find { |key| key.kid == kid }
raise JWT::VerificationError, 'Key not found in JWKS' unless jwk

public_key = jwk.public_key

options = {
algorithm: 'RS256',
verify_expiration: true,
verify_aud: false,
verify_iss: true,
iss: EXPECTED_ISS
}

# Returns [payload, header] array if valid
JWT.decode(token, public_key, true, options)
rescue JWT::ExpiredSignature
raise JWT::VerificationError, 'Token has expired'
rescue JWT::InvalidIssuerError
raise JWT::VerificationError, 'Token issuer mismatch'
rescue JWT::DecodeError => e
raise JWT::VerificationError, "Invalid JWT: #{e.message}"
end
# rubocop:enable Metrics/MethodLength
end
19 changes: 15 additions & 4 deletions app/controllers/fees_controller.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
require 'jwt'

class FeesController < ApplicationController

# This will be needed for transaction_complete since Paypal will hit that
protect_from_forgery with: :null_session

# rubocop:disable Metrics/MethodLength
def index
@jwt = params.require(:jwt)
decoded_token = JWT.decode @jwt, nil, false
@alma_id = decoded_token.first['userName']
@fees = FeesPayment.new(alma_id: @alma_id)
rescue JWT::DecodeError
payload = AlmaJwtValidator.decode_and_verify_jwt(@jwt)
@alma_id = payload.first['userName']
begin
@fees = FeesPayment.new(alma_id: @alma_id)
rescue StandardError => e
Rails.logger.warn "FeesPayment failed: #{e.message}"
redirect_to(action: :transaction_error) and return
end
rescue ActionController::ParameterMissing
redirect_to 'https://www.lib.berkeley.edu/find/borrow-renew?section=pay-fees', allow_other_host: true
rescue JWT::DecodeError => e
Rails.logger.warn "JWT verification failed: #{e.message}"
redirect_to(action: :transaction_error)
end
# rubocop:enable Metrics/MethodLength

def efee
@jwt = params.require(:jwt)
Expand Down
2 changes: 1 addition & 1 deletion spec/data/fees/alma-fees-jwt.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJQcmltYSIsImp0aSI6IkMyOTIyMEQ2RTFCQTk4NDMzOEMzQTRDMEFCOTUwOUY5LmFwZDAzLm5hMDcucHJvZC5hbG1hLmRjMDEuaG9zdGVkLmV4bGlicmlzZ3JvdXAuY29tOjE4MDEiLCJ1c2VyTmFtZSI6IjEwMzM1MDI2IiwiZGlzcGxheU5hbWUiOiJTdWxsaXZhbiwgU3RldmVuIiwidXNlciI6IjI2Mzc3MjgwNTAwMDY1MzIiLCJ1c2VyR3JvdXAiOiJMSUJTVEFGRiIsImluc3RpdHV0aW9uIjoiMDFVQ1NfQkVSIiwidXNlcklwIjoiNzMuNzEuMTM4LjE3IiwiYXV0aGVudGljYXRpb25Qcm9maWxlIjoiQ0FTIiwiYXV0aGVudGljYXRpb25TeXN0ZW0iOiJDQVMiLCJsYW5ndWFnZSI6ImVuIiwic2FtbFNlc3Npb25JbmRleCI6IiIsInNhbWxOYW1lSWQiOiIiLCJvbkNhbXB1cyI6ImZhbHNlIiwic2lnbmVkSW4iOiJ0cnVlIiwidmlld0lkIjoiMDFVQ1NfQkVSOlVDQiJ9.Xus3sbFX8IHLPrV5_5YY8gbtBXzC48xLOu3XsMtQaMw
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6IjEwMzM1MDI2In0.23uMX0G7rPdgXarjFtlNUhxJJKGXDnlVNJpS34E0Vfg
20 changes: 14 additions & 6 deletions spec/request/fees_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ def base_url_for(user_id = nil)
let(:request_headers) { { 'Accept' => 'application/json', 'Authorization' => "apikey #{alma_api_key}" } }

before do
allow(Rails.application.config).to receive(:alma_api_key).and_return(alma_api_key)
allow(AlmaJwtValidator).to receive(:decode_and_verify_jwt).and_return(
[{ 'userName' => '10335026' }]
)
allow(Rails.application.config).to receive_messages(
alma_api_key: alma_api_key,
alma_jwt_secret: 'fake-jwt-secret'
)
end

it 'shows a Bad Request error if request has no jwt' do
it 'redirects to the fallback URL if there is no jwt' do
get fees_path
expect(response).to have_http_status(:bad_request)
expect(response).to redirect_to('https://www.lib.berkeley.edu/find/borrow-renew?section=pay-fees')
end

it 'redirects to error page if request has a non-existant alma id' do
stub_request(:get, "#{base_url_for}fees")
user_id = '10335026'
stub_request(:get, "#{base_url_for(user_id)}/fees")
.with(headers: request_headers)
.to_return(status: 404, body: '')

Expand Down Expand Up @@ -53,9 +60,10 @@ def base_url_for(user_id = nil)
end

it 'payments page redirects to index if no fee was selected for payment' do
post '/fees/payment', params: { jwt: File.read('spec/data/fees/alma-fees-jwt.txt') }
jwt = File.read('spec/data/fees/alma-fees-jwt.txt').strip
post '/fees/payment', params: { jwt: jwt }
expect(response).to have_http_status(:found)
expect(response).to redirect_to("#{fees_path}?jwt=#{File.read('spec/data/fees/alma-fees-jwt.txt')}")
expect(response).to redirect_to("#{fees_path}?jwt=#{jwt}")
end

it 'successful transaction_complete returns status 200' do
Expand Down
Loading