diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2e52590d..c7976002 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,8 +4,13 @@ class ApplicationController < ActionController::Base include ActionController::HttpAuthentication::Bearer rescue_from CanCan::AccessDenied, with: :rescue_can_can + before_action :set_origin_header - protected + private + + def set_origin_header + response.headers['Access-Control-Allow-Origin'] = '*' + end def current_user @current_user ||= if has_basic_credentials?(request) 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/controllers/media_controller.rb b/app/controllers/media_controller.rb index fe252ace..b78c4240 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -2,7 +2,6 @@ # API for delivering streaming media via stacks class MediaController < ApplicationController before_action :load_media - before_action :set_origin_header, except: [:auth_check] before_action :set_cors_headers, only: [:auth_check] rescue_from ActionController::MissingFile do @@ -29,12 +28,11 @@ def auth_check private - # We do not rely on the web server to set Access-Control-Allow-Origin for *any* /media request, - # so we set it manually ourselves. - def set_origin_header - response.headers['Access-Control-Allow-Origin'] = '*' - end - + # In order for media authentication to work, the wowza server must have + # Access-Control-Allow-Credentials header set (which is set by default when CORS is enabled in wowza), + # which means that Access-Control-Allow-Origin cannot be set to * (wowza default) and instead + # needs to specify a host (e.g. the embed server of choice, presumably used in purl with + # particular stacks). This means that only the specified host will be granted credentialed requests. def set_cors_headers response.headers['Access-Control-Allow-Origin'] = Settings.cors.allow_origin_url response.headers['Access-Control-Allow-Credentials'] = 'true' 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/config/settings.yml b/config/settings.yml index 681717cd..2ea963db 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,6 +1,6 @@ stacks: storage_root: /stacks - djatoka_url: 'http://imageserver-b.stanford.edu/adore-djatoka/resolver' + djatoka_url: 'http://localhost:8080/resolver' purl: url: 'https://purl.stanford.edu/' 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