Skip to content

Commit

Permalink
Add a IIIF v2 token service
Browse files Browse the repository at this point in the history
  • Loading branch information
jcoyne committed Dec 12, 2023
1 parent 2ad9f22 commit e358a69
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 18 deletions.
22 changes: 11 additions & 11 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2023-12-06 16:20:31 UTC using RuboCop version 1.58.0.
# on 2023-12-11 17:46:51 UTC using RuboCop version 1.58.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -25,12 +25,12 @@ Metrics/AbcSize:
Metrics/CyclomaticComplexity:
Max: 10

# Offense count: 13
# Offense count: 15
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Max: 25

# Offense count: 25
# Offense count: 26
RSpec/AnyInstance:
Exclude:
- 'spec/controllers/media_controller_spec.rb'
Expand All @@ -40,11 +40,12 @@ RSpec/AnyInstance:
- 'spec/requests/file_auth_request_spec.rb'
- 'spec/requests/file_spec.rb'
- 'spec/requests/iiif/auth/v1/tokens_spec.rb'
- 'spec/requests/iiif/auth/v2/tokens_spec.rb'
- 'spec/requests/iiif_auth_request_spec.rb'
- 'spec/requests/iiif_spec.rb'
- 'spec/requests/media_auth_request_spec.rb'

# Offense count: 68
# Offense count: 65
# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
Expand All @@ -71,12 +72,11 @@ RSpec/DescribedClass:
- 'spec/models/stacks_media_token_spec.rb'
- 'spec/models/user_spec.rb'

# Offense count: 4
# Offense count: 3
# This cop supports safe autocorrection (--autocorrect).
RSpec/EmptyLineAfterExampleGroup:
Exclude:
- 'spec/abilities/ability_spec.rb'
- 'spec/controllers/legacy_image_service_controller_spec.rb'
- 'spec/requests/file_auth_request_spec.rb'

# Offense count: 34
Expand Down Expand Up @@ -113,7 +113,7 @@ RSpec/EmptyLineAfterSubject:
- 'spec/models/stacks_image_spec.rb'
- 'spec/services/iiif_metadata_service_spec.rb'

# Offense count: 22
# Offense count: 32
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 16
Expand Down Expand Up @@ -155,11 +155,11 @@ RSpec/MessageSpies:
- 'spec/controllers/file_controller_spec.rb'
- 'spec/controllers/media_controller_spec.rb'

# Offense count: 64
# Offense count: 75
RSpec/MultipleExpectations:
Max: 12

# Offense count: 64
# Offense count: 63
# Configuration parameters: EnforcedStyle, IgnoreSharedExamples.
# SupportedStyles: always, named_only
RSpec/NamedSubject:
Expand All @@ -177,7 +177,7 @@ RSpec/NamedSubject:
- 'spec/services/iiif_metadata_service_spec.rb'
- 'spec/services/media_authentication_json_spec.rb'

# Offense count: 47
# Offense count: 46
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 5
Expand All @@ -199,7 +199,7 @@ RSpec/SubjectStub:
Exclude:
- 'spec/models/stacks_media_token_spec.rb'

# Offense count: 14
# Offense count: 13
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
Expand Down
65 changes: 65 additions & 0 deletions app/controllers/iiif/auth/v2/token_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

# API to create IIIF Authentication access tokens
module Iiif
module Auth
module V2
# Creates tokens for IIIF auth v2
# https://iiif.io/api/auth/2.0/#access-token-service
class TokenController < ApplicationController
skip_forgery_protection

# Returns an HTML response that posts back to the parent window
# See {https://iiif.io/api/auth/2.0/#workflow-from-the-browser-client-perspective}
def create
params.require(:origin)
params.require(:messageId)

token = mint_bearer_token if token_eligible_user?

@message = if token
{
"@context" => "http://iiif.io/api/auth/2/context.json",
type: 'AuthAccessToken2',
accessToken: token,
expiresIn: 3600,
messageId: params[:messageId]
}
else
{
"@context" => "http://iiif.io/api/auth/2/context.json",
type: 'AuthAccessTokenError2',
profile: 'missingAspect',
error: 'missingCredentials',
heading: "Missing credentials",
messageId: params[:messageId]
}
end

# The browser-based interaction requires using iframes
# We disable this header (added by default) entirely to ensure
# that IIIF viewers embedded by iframes in other pages will
# work as expected.
response.headers['X-Frame-Options'] = ""

@origin = params[:origin]

render 'create', layout: false
end

private

# An authenticated user can retrieve a token if they are logged in with webauth,
# or are accessing material from a location-specific kiosk.
# Other anonymous users are not eligible.
def token_eligible_user?
current_user.webauth_user? || current_user.location?
end

def mint_bearer_token
"#{ActionController::HttpAuthentication::Token::TOKEN_KEY}#{current_user.token.to_s.inspect}"
end
end
end
end
end
10 changes: 10 additions & 0 deletions app/views/iiif/auth/v2/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>
4 changes: 2 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
get '/image/auth/:id/:file_name' => 'legacy_image_service#show'
end

# IIIF Probe Service
# IIIF Auth V2
get '/iiif/auth/v2/token' => 'iiif/auth/v2/token#create'
get '/iiif/auth/v2/probe' => 'iiif/auth/v2/probe_service#show'

end
9 changes: 4 additions & 5 deletions spec/requests/iiif/auth/v2/probe_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,12 @@
stub_rights_xml(stanford_restricted_rights_xml)
end

context 'when the user is logged in as a Stanford user' do
context 'when the user has a bearer token with the ldap group' do
let(:user_webauth_stanford_no_loc) { User.new(webauth_user: true, ldap_groups: %w[stanford:stanford]) }
let(:current_user) { user_webauth_stanford_no_loc }
let(:token) { user_webauth_stanford_no_loc.token }

before do
allow_any_instance_of(Iiif::Auth::V2::ProbeServiceController).to receive(:current_user).and_return(current_user)
get "/iiif/auth/v2/probe?id=#{stacks_uri_param}"
get "/iiif/auth/v2/probe?id=#{stacks_uri_param}", headers: { 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end

it 'returns a success response' do
Expand All @@ -147,7 +146,7 @@
end
end

context 'when the user is not logged in as a Stanford user' do
context 'when the user does not provide a token' do
before do
get "/iiif/auth/v2/probe?id=#{stacks_uri_param}"
end
Expand Down
53 changes: 53 additions & 0 deletions spec/requests/iiif/auth/v2/tokens_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'IIIF auth v2 tokens' do
let(:user) { User.new(anonymous_locatable_user: true) }

before do
allow_any_instance_of(Iiif::Auth::V2::TokenController).to receive(:current_user).and_return(user)
end

describe '#create' do
let(:user) { User.new id: 'xyz', webauth_user: true }

before do
get '/iiif/auth/v2/token?origin=http://example.edu/&messageId=1'
end

it 'posts the response' do
expect(response.response_code).to eq 200
expect(response.headers['X-Frame-Options']).to eq ''
payload = JSON.parse(/({.*\})/.match(response.body)[1])
expect(payload).to include(
{ "@context" => "http://iiif.io/api/auth/2/context.json",
"type" => "AuthAccessToken2", 'accessToken' => be_present,
'messageId' => '1', 'expiresIn' => 3600 }
)
end

context 'when there is an error' do
let(:user) { User.new id: 'xyz', webauth_user: false }

it 'posts the response' do
expect(response.response_code).to eq 200
expect(response.headers['X-Frame-Options']).to eq ''
payload = JSON.parse(/({.*\})/.match(response.body)[1])
expect(payload).to include(
{ "@context" => "http://iiif.io/api/auth/2/context.json",
"type" => "AuthAccessTokenError2", 'profile' => 'missingAspect',
'messageId' => '1',
'heading' => 'Missing credentials' }
)
end
end

context 'when the origin parameter is missing' do
it 'returns a 400 error' do
get '/iiif/auth/v2/token?messageId=1'
expect(response).to have_http_status(:bad_request)
end
end
end
end

0 comments on commit e358a69

Please sign in to comment.