Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IIIF Authentication 1.0 browse-based client application authentication #124

Merged
merged 4 commits into from
Jun 6, 2017
Merged
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
7 changes: 6 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/iiif_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
}
Expand Down
63 changes: 48 additions & 15 deletions app/controllers/iiif_token_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions app/controllers/media_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
10 changes: 10 additions & 0 deletions app/views/iiif_token/create.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<body>
<script>
window.parent.postMessage(
<%= @message.to_json.html_safe %>,
<%= @origin.to_json.html_safe %>
);
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion config/settings.yml
Original file line number Diff line number Diff line change
@@ -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/'
Expand Down
15 changes: 15 additions & 0 deletions spec/controllers/iiif_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,34 @@ 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
it 'the tile height/width is 256' do
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
Expand Down
86 changes: 64 additions & 22 deletions spec/controllers/iiif_token_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -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(/<html/)
end
end

it 'returns the token response' do
expect(subject.status).to eq 200
context 'missing the origin header' do
subject do
get :create, params: { messageId: '1' }
end

data = JSON.parse(subject.body)
expect(data['accessToken']).not_to be_blank
expect(data['tokenType']).to eq 'Bearer'
expect(data['expiresIn']).to be > 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
Expand Down