Skip to content

Commit

Permalink
Merge pull request #124 from sul-dlss/iiif-auth-10
Browse files Browse the repository at this point in the history
Implement IIIF Authentication 1.0 browse-based client application authentication
  • Loading branch information
tingulfsen committed Jun 6, 2017
2 parents 6e5ba37 + 98794a7 commit 778ee55
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 48 deletions.
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

0 comments on commit 778ee55

Please sign in to comment.