From 98794a75de9d70209e50f7a24157c167eb2749f1 Mon Sep 17 00:00:00 2001 From: Chris Beer Date: Mon, 5 Jun 2017 16:26:33 -0700 Subject: [PATCH] Implement IIIF Authentication 1.0 browse-based client application authentication --- app/controllers/iiif_controller.rb | 4 +- app/controllers/iiif_token_controller.rb | 63 ++++++++++---- app/views/iiif_token/create.html.erb | 10 +++ spec/controllers/iiif_controller_spec.rb | 15 ++++ .../controllers/iiif_token_controller_spec.rb | 86 ++++++++++++++----- 5 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 app/views/iiif_token/create.html.erb diff --git a/app/controllers/iiif_controller.rb b/app/controllers/iiif_controller.rb index 1a52ce2b..13cd6393 100644 --- a/app/controllers/iiif_controller.rb +++ b/app/controllers/iiif_controller.rb @@ -111,12 +111,12 @@ def image_info unless anonymous_ability.can? :download, current_image info['service'] = { '@id' => iiif_auth_api_url, - 'profile' => 'http://iiif.io/api/auth/0/login', + 'profile' => 'http://iiif.io/api/auth/1/login', 'label' => 'Stanford-affiliated? Login to view', 'service' => [ { '@id' => iiif_token_api_url, - 'profile' => 'http://iiif.io/api/auth/0/token' + 'profile' => 'http://iiif.io/api/auth/1/token' } ] } diff --git a/app/controllers/iiif_token_controller.rb b/app/controllers/iiif_token_controller.rb index d948eb2e..8943a23d 100644 --- a/app/controllers/iiif_token_controller.rb +++ b/app/controllers/iiif_token_controller.rb @@ -5,38 +5,71 @@ def create write_bearer_token_cookie(token) if token + @message = if token + { + accessToken: token, + tokenType: 'Bearer', + expiresIn: 3600 + } + else + { error: 'missingCredentials', description: '' } + end + + if browser_based_client_auth? + create_for_browser_based_client_application_auth + else + create_for_json_access_token_auth(token) + end + end + + private + + # Handle IIIF Authentication 1.0 browser-based client application requests + # See {http://iiif.io/api/auth/1.0/#interaction-for-browser-based-client-applications} + def create_for_browser_based_client_application_auth + browser_params.require(:origin) + + # The browser-based interaction requires using iframes + response.headers['X-Frame-Options'] = "ALLOW-FROM #{browser_params[:origin]}" + + @message[:messageId] = browser_params[:messageId] + + @origin = browser_params[:origin] + + render 'create', layout: false + end + + # Handle IIIF Authentication 1.0 JSON Access Token requests + # See {http://iiif.io/api/auth/1.0/#the-json-access-token-response} + def create_for_json_access_token_auth(token) respond_to do |format| format.html { redirect_to callback: callback_value, format: 'js' } format.js do - response = if token - { - accessToken: token, - tokenType: 'Bearer', - expiresIn: 3600 - } - else - { error: 'missingCredentials', description: '' } - end - status = if callback_value || token :ok else :unauthorized end - render json: response.to_json, callback: callback_value, status: status + render json: @message.to_json, callback: callback_value, status: status end end end - private - - def allowed_params + def json_params params.permit(:callback) end + def browser_params + params.permit(:messageId, :origin) + end + + def browser_based_client_auth? + browser_params[:messageId].present? + end + def callback_value - allowed_params[:callback] + json_params[:callback] end def mint_bearer_token diff --git a/app/views/iiif_token/create.html.erb b/app/views/iiif_token/create.html.erb new file mode 100644 index 00000000..fea6e2b3 --- /dev/null +++ b/app/views/iiif_token/create.html.erb @@ -0,0 +1,10 @@ + + + + + diff --git a/spec/controllers/iiif_controller_spec.rb b/spec/controllers/iiif_controller_spec.rb index 10e22ab3..3b407f26 100644 --- a/spec/controllers/iiif_controller_spec.rb +++ b/spec/controllers/iiif_controller_spec.rb @@ -117,12 +117,18 @@ def maybe_downloadable? context 'when the image is downloadable' do before do allow(controller).to receive(:can?).with(:download, stub_metadata_object).and_return(true) + allow(controller.send(:anonymous_ability)).to receive(:can?) + .with(:download, stub_metadata_object).and_return(true) end it 'the tile height/width is 1024' do expect(image_info[:tile_height]).to eq 1024 expect(image_info[:tile_width]).to eq 1024 end + + it 'omits the authentication service' do + expect(image_info['service']).not_to be_present + end end context 'when the image is not downloadable' do @@ -130,6 +136,15 @@ def maybe_downloadable? expect(image_info[:tile_height]).to eq 256 expect(image_info[:tile_width]).to eq 256 end + + it 'advertises an authentication service' do + expect(image_info['service']).to be_present + expect(image_info['service']['profile']).to eq 'http://iiif.io/api/auth/1/login' + expect(image_info['service']['@id']).to eq iiif_auth_api_url + + expect(image_info['service']['service'].first['profile']).to eq 'http://iiif.io/api/auth/1/token' + expect(image_info['service']['service'].first['@id']).to eq iiif_token_api_url + end end end end diff --git a/spec/controllers/iiif_token_controller_spec.rb b/spec/controllers/iiif_token_controller_spec.rb index 68058cb7..a0436d98 100644 --- a/spec/controllers/iiif_token_controller_spec.rb +++ b/spec/controllers/iiif_token_controller_spec.rb @@ -1,46 +1,88 @@ require 'rails_helper' describe IiifTokenController do - describe '#create' do - subject do - get :create, format: :js - end + render_views + describe '#create' do let(:user) { User.new(anonymous_locatable_user: true) } before do allow(controller).to receive(:current_user).and_return(user) end - context 'HTML format' do + context 'browser-based interaction' do + let(:user) { User.new id: 'xyz' } + subject do - get :create, params: { format: :html } + get :create, params: { origin: 'http://example.edu/', messageId: '1' } end - it 'redirects to JSON' do - expect(subject).to redirect_to format: :js + it 'sets the X-Frame-Options header' do + expect(subject.headers['X-Frame-Options']).to eq 'ALLOW-FROM http://example.edu/' end - end - context 'with a user' do - let(:user) { User.new id: 'xyz' } + it 'assigns the message and origin parameters' do + expect(subject.response_code).to eq 200 + expect(assigns(:origin)).to eq 'http://example.edu/' + expect(assigns(:message)).to include messageId: '1', accessToken: be_present + end + + context 'other formats' do + subject do + get :create, params: { origin: 'http://example.edu/', messageId: '1', format: :js } + end + + it 'renders HTML anyway' do + expect(subject.body).to match(/ 0 + it 'returns a 400 error' do + expect { subject.response }.to raise_error ActionController::ParameterMissing + end end end - context 'with an anonymous user' do - it 'returns the error response' do - expect(subject.status).to eq 401 + context 'JSON API interaction' do + subject do + get :create, params: { format: :js } + end + + context 'HTML format' do + subject do + get :create, params: { format: :html } + end + + it 'redirects to JSON' do + expect(subject).to redirect_to format: :js + end + end + + context 'with a user' do + let(:user) { User.new id: 'xyz' } + + it 'returns the token response' do + expect(subject.status).to eq 200 + + data = JSON.parse(subject.body) + expect(data['accessToken']).not_to be_blank + expect(data['tokenType']).to eq 'Bearer' + expect(data['expiresIn']).to be > 0 + end + end + + context 'with an anonymous user' do + it 'returns the error response' do + expect(subject.status).to eq 401 - data = JSON.parse(subject.body) - expect(data['error']).to eq 'missingCredentials' + data = JSON.parse(subject.body) + expect(data['error']).to eq 'missingCredentials' + end end end end