The authorize()
delegate method can be used to implement authorization logic ranging from simple to complex. A skeleton with documented parameters and return values is present in the delegates.rb.sample file. By default, it just returns true
, authorizing all requests.
The authorization method will be called upon every request to a public endpoint, and should therefore be written to be efficient.
Implementations should assume that the underlying source image exists. The existence check will happen separately.
class CustomDelegate
def authorize(options = {})
scale = context['operations'].find{ |op| op['class'] == 'Scale' }
if scale
max_scale = 0.5
return scale['width'] <= full_size['width'] * max_scale and
scale['height'] <= full_size['height'] * max_scale
end
false
end
end
class CustomDelegate
def authorize(options = {})
identifier = context['identifier']
# Allow only identifiers that don't include "_restricted"
return !identifier.include?('_restricted')
# Allow only identifiers that start with "_public"
return identifier.start_with?('public_')
# Allow only identifiers matching a regex
return identifier.match(/^image[5-9][0-9]/)
end
end
class CustomDelegate
def authorize(options = {})
context['output_format'] == 'image/jpeg'
end
end
This is not foolproof, as user agents can be faked.
class CustomDelegate
def authorize(options = {})
headers = context['request_headers']
agent = headers.find{ |h, v| h.downcase == 'user-agent' }
agent.start_with?('MyAllowedUserAgent/')
end
end
class CustomDelegate
def authorize(options = {})
headers['X-MyToken'] == ... # write code to authorize the token
end
end
In this example, requests for images containing any part of the bottom right quadrant of the source image will be denied.
(Also see redaction.)
class CustomDelegate
def authorize(options = {})
crop = context['operations'].find{ |op| op['class'] == 'Crop' }
if crop
max_x = full_size['width'] / 2.0
max_y = full_size['height'] / 2.0
return !(crop['x'] + crop['width'] > max_x and
crop['y'] + crop['height'] > max_y)
end
false
end
end
With HTTP Basic authentication, the server sends an HTTP 401 (Unauthorized) status code along with a WWW-Authenticate: Basic
header, which tells the client that subsequent requests must include an Authorization
header containing encoded credentials. (Web browsers will normally prompt for these and store them for a time.)
HTTP Basic should only be used over SSL/TLS.
require 'base64'
class CustomDelegate
def authorize(options = {})
header = context['request_headers']
.select{ |name, value| name.downcase == 'authorization' }
.values.first
return true if authenticate_basic(header)
return {
'status_code' => 401,
'challenge' => 'Basic realm="MyRealm" charset="UTF-8"'
}
end
def authenticate_basic(header)
if header&.start_with?('Basic ')
encoded = header[6..header.length - 1]
creds = Base64.decode64(encoded).split(':')
if creds.length > 1
return (creds[0] == 'my_user' and creds[1] == 'my_secret')
end
end
false
end
end
See Tiered Access.
class CustomDelegate
def authorize(options = {})
{
'status_code' => 303, # "See Other"
'location' => 'http://example.org/some-other-url'
}
end
end
See Overlays.
The IIIF Authentication API 1.0 describes general patterns of access-restricting IIIF resources, including images, in a standard way. The authorize()
delegate method can be used in ways that conform to this API's notions of "all or nothing" and "tiered" access.
For both of these:
extra_iiif_2_information_response_keys()
must return the authentication services used by the API, which you must provide.authorize()
method must accept bearer tokens from the client, and return a status of 200 (or true
) for authorized requests; 302 for redirects; and 401 for unauthorized requests.All or Nothing Access grants access to either the full-quality image, or nothing.
class CustomDelegate
def authorize(options = {})
header = context['request_headers']
.select{ |name, value| name.downcase == 'authorization' }
.values.first
if header&.start_with?('Bearer ')
token = header[7..header.length - 1]
# Authorize the token and return true if it's valid.
end
return {
'status_code' => 401,
'challenge' => 'Bearer charset="UTF-8"'
}
end
end
Tiered Access can be used to redirect to any of a number of "virtual" reduced-quality versions ("tiers") of an image based on authorization status.
The IIIF Authorization API is agnostic about how a quality tier system should work. Cantaloupe uses a resolution-based system that limits the maximum available resolution. For example, a fully-authorized user might be able to access the full resolution of an image, whereas an unauthorized user might be able to access only a half-resolution version.
This works transparently with zooming viewer clients, limiting the maximum available zoom level.
By default (see below), URI identifiers of scale-constrained images have a suffix in the format -numerator:denominator
. So, for example, if the client requests image.tif/full/max/0/default.jpg
, but is only authorized to access a half-resolution version, it is redirected to image.tif-1:2/full/max/0/default.jpg
, from which it is allowed to retrieve the full size of a dynamically half-sized version of image.tif
.
In 4.x versions 4.1.9 and later, the scale constraint format can be customized using the scale_constraint_suffix.pattern
and scale_constraint_suffix.format
configuration keys. These are a stopgap solution that applies only to the 4.x series and is replaced by meta-identifier transformers in the 5.x series.
class CustomDelegate
# The maximum scale allowed to unauthorized clients.
MAX_UNAUTHORIZED_R = Rational(1, 2)
def authorize(options = {})
header = context['request_headers']
.select{ |name, value| name.downcase == 'authorization' }
.values.first
if header&.start_with?('Bearer ')
token = header[7..header.length]
if valid?(token)
# Get the current scale constraint fraction, which ultimately comes
# from the identifier URI path component, which is conveniently
# made available for you in the delegate script context.
# This is a two-element array of numerator and denominator integers.
scale_constraint = context['scale_constraint']
# If it exists, convert it to a Rational (Ruby fraction).
# Otherwise, use 1/1.
scale_constraint_r = scale_constraint ?
Rational(*scale_constraint) : Rational(1)
# If the requested scale constraint is larger than authorized,
# redirect to the authorized scale constraint.
if scale_constraint_r > MAX_UNAUTHORIZED_R
return {
'status_code' => 302,
'scale_numerator' => MAX_UNAUTHORIZED_R.numerator,
'scale_denominator' => MAX_UNAUTHORIZED_R.denominator
}
else
return true
end
end
end
return {
'status_code' => 401,
'challenge' => 'Bearer charset="UTF-8"'
}
end
def valid?(token)
# check whether the token is valid and return true or false
end
end