triannon 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +119 -10
- data/app/controllers/triannon/application_controller.rb +73 -58
- data/app/controllers/triannon/auth_controller.rb +103 -154
- data/config/triannon.yml +3 -3
- data/lib/generators/triannon/install_generator.rb +6 -9
- data/lib/triannon/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 051a29bfe3ab0af248eae2e88c87a597d5e2f99d
|
4
|
+
data.tar.gz: eb636792129fe4969f33f5dd2f86248b4b758608
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8989ea886e8e7af83cac9b3c70bd65f37720f0e6322d06f59ff0eb2f528ca46b7ff33d4fe47a51dc902933f046411591a7eca2933a83aece7d9bdbe07da0e98a
|
7
|
+
data.tar.gz: 59c72b020f1e901372cbc898899c3022eaf3a8965a1acf428bd88e57aae7c5e277165704729d64ac7c3fbe19b7df372aa45e130befed0bbb19f11a93b1a89f79
|
data/README.md
CHANGED
@@ -24,16 +24,44 @@ Then run the triannon generator:
|
|
24
24
|
$ rails g triannon:install
|
25
25
|
```
|
26
26
|
|
27
|
+
|
28
|
+
## Configuration
|
29
|
+
|
27
30
|
Edit the `config/triannon.yml` file:
|
28
31
|
|
29
32
|
* `ldp:` Properties of LDP server
|
30
33
|
* `url:` the baseurl of LDP server
|
31
|
-
* `uber_container:` name of an LDP Basic Container holding
|
32
|
-
* `anno_containers:`
|
34
|
+
* `uber_container:` name of an LDP Basic Container holding all `anno_containers`
|
35
|
+
* `anno_containers:` names of LDP Basic Containers holding annos
|
33
36
|
* `solr_url:` Points to the baseurl of Solr instance configured for Triannon
|
34
37
|
* `triannon_base_url:` Used as the base url for all annotations hosted by your Triannon server. Identifiers from the LDP server will be appended to this base-url. Generally something like "https://your-triannon-rails-box/annotations", as "/annotations" is added to the path by the Triannon gem
|
35
38
|
|
36
|
-
|
39
|
+
#### Authorization for Containers:
|
40
|
+
```
|
41
|
+
anno_containers:
|
42
|
+
foo:
|
43
|
+
bar:
|
44
|
+
auth:
|
45
|
+
users: []
|
46
|
+
workgroups:
|
47
|
+
- org:wg-A
|
48
|
+
- org:wg-B
|
49
|
+
```
|
50
|
+
|
51
|
+
Authorization applies only to POST and DELETE requests. In this example, the `foo` container requires no authorization (all operations are allowed). On the other hand, the `bar` container requires authorization. There are no authorized `users`, but two `workgroups` are authorized to modify annos in the container ('org:wg-A' and 'org:wg-B').
|
52
|
+
|
53
|
+
#### Authorized Clients:
|
54
|
+
```
|
55
|
+
authorized_clients:
|
56
|
+
clientA: secretA
|
57
|
+
# expiry values are in seconds
|
58
|
+
client_token_expiry: 120
|
59
|
+
access_token_expiry: 3600
|
60
|
+
```
|
61
|
+
|
62
|
+
When authorization is required on a container, there must be at least one authorized client. The client credentials are used to validate an authorized client that will present requests on behalf of an authorized user or workgroup (see below for details on the authorization workflow). The client credentials are not specific to any container.
|
63
|
+
|
64
|
+
#### Generate the root annotations containers on your LDP server:
|
37
65
|
|
38
66
|
```console
|
39
67
|
$ rake triannon:create_root_containers
|
@@ -43,7 +71,7 @@ $ rake triannon:create_root_containers
|
|
43
71
|
|
44
72
|
NOTE: you MUST create the root containers before creating any annotations. All annotation MUST be created as a child of a root container.
|
45
73
|
|
46
|
-
|
74
|
+
#### Caching jsonld context documents:
|
47
75
|
|
48
76
|
* by using Rack::Cache for RestClient:
|
49
77
|
|
@@ -72,7 +100,11 @@ RestClient.enable Rack::Cache,
|
|
72
100
|
|
73
101
|
## Client Interactions with Triannon
|
74
102
|
|
75
|
-
###
|
103
|
+
### READ operations
|
104
|
+
|
105
|
+
GET requests do not require authorization, even for containers that are configured with authorization for POST and DELETE requests.
|
106
|
+
|
107
|
+
#### Get a list of annos
|
76
108
|
as a IIIF Annotation List (see http://iiif.io/api/presentation/2.0/#other-content-resources)
|
77
109
|
|
78
110
|
* `GET`: `http://(host)/annotations/search?targetUri=some.url.org`
|
@@ -91,7 +123,7 @@ Search Parameters:
|
|
91
123
|
* also supports turtle, rdfxml, json, html
|
92
124
|
* `Accept`: `application/x-turtle`
|
93
125
|
|
94
|
-
|
126
|
+
#### Get a list of annos in a particular root container
|
95
127
|
as a IIIF Annotation List (see http://iiif.io/api/presentation/2.0/#other-content-resources)
|
96
128
|
|
97
129
|
* `GET`: `http://(host)/annotations/(root container)/search?targetUri=some.url.org`
|
@@ -104,7 +136,7 @@ Search Parameters as above.
|
|
104
136
|
* also supports turtle, rdfxml, json, html
|
105
137
|
* `Accept`: `application/x-turtle`
|
106
138
|
|
107
|
-
|
139
|
+
#### Get a particular anno
|
108
140
|
`GET`: `http://(host)/annotations/(root container)/(anno_id)`
|
109
141
|
|
110
142
|
NOTE: you may need to URL encode the anno_id (e.g. "6f%2F0e%2F79%2F92%2F6f0e7992-83f5-4f31-8bb7-94a23465fdfb" instead of "6f/0e/79/92/6f0e7992-83f5-4f31-8bb7-94a23465fdfb"), particularly from a web browser.
|
@@ -136,7 +168,11 @@ You can also use this method (with the correct HTTP Accept header):
|
|
136
168
|
|
137
169
|
Note that OA (Open Annotation) is the default context if none is specified.
|
138
170
|
|
139
|
-
###
|
171
|
+
### WRITE operations
|
172
|
+
|
173
|
+
When a container is configured with authorization, it applies to POST and DELETE requests. For these requests, a valid access token is required in the request header, i.e. 'Authorization': 'Bearer {token_here}' (see details below on how to obtain an access token).
|
174
|
+
|
175
|
+
#### Create an anno
|
140
176
|
|
141
177
|
Note that annos must be created in an existing root container.
|
142
178
|
|
@@ -155,11 +191,84 @@ Note that annos must be created in an existing root container.
|
|
155
191
|
* `Link`: `http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"`
|
156
192
|
* note that the "type" part is optional and refers to the type of the rel, which is the reference for all json-ld contexts.
|
157
193
|
|
158
|
-
|
194
|
+
#### Delete an anno
|
159
195
|
`DELETE`: `http://(host)/annotations/(root container)/(anno_id)`
|
160
196
|
|
161
|
-
NOTE:
|
197
|
+
NOTE: URL encode the anno_id (e.g. "6f%2F0e%2F79%2F92%2F6f0e7992-83f5-4f31-8bb7-94a23465fdfb" instead of "6f/0e/79/92/6f0e7992-83f5-4f31-8bb7-94a23465fdfb")
|
198
|
+
|
199
|
+
### Authorization
|
200
|
+
|
201
|
+
The triannon authorization is modeled on IIIF proposals, see
|
202
|
+
- http://iiif.io/api/auth/
|
203
|
+
- https://github.com/IIIF/auth
|
204
|
+
|
205
|
+
The authorization workflow accepts json and returns json (not json-ld). It involves three phases and triannon manages authorization using cookies, which must be retained across requests.
|
206
|
+
|
207
|
+
1. Obtain a client authorization code (short-lived token).
|
208
|
+
2. Use client authorization code to submit login credentials for authorized user or workgroup.
|
209
|
+
3. Obtain an access token for submitting or deleting annotations on behalf of the authorized user or workgroup.
|
210
|
+
|
211
|
+
### Authorization Examples
|
212
|
+
|
213
|
+
Let's assume we have the authorization configuration noted above.
|
214
|
+
|
215
|
+
#### Authorization using `curl`
|
216
|
+
```sh
|
217
|
+
# client and user credentials (consistent with configs)
|
218
|
+
client='{"clientId":"clientA","clientSecret":"secretA"}'
|
219
|
+
user='{"userId":"userA","workgroups":"org:wg-A"}'
|
220
|
+
|
221
|
+
# 1. POST client credentials to '/auth/client_identity'
|
222
|
+
# to get client authorization code (save cookies)
|
223
|
+
code=$(curl -s -X POST -H "Content-Type: application/json" \
|
224
|
+
-c cookies.txt -d $client http://localhost:3000/auth/client_identity)
|
225
|
+
echo $code
|
226
|
+
#>> {"authorizationCode":"VjZQODJmbGtXU1VUMzdpcDhPemt4bjA2N3A4ZU1xQy9laTl3Uk9sUUd5YlZDMmtuZ05COFFtUVpHSGZzMGxLeC0ta2hDemRXdUdNQy9sMVJVeFJwdzN2dz09--8405018ec9fc7d751726417101963f18ee175c71"}
|
227
|
+
client_code=$(echo $code | sed -e 's/{"authorizationCode":"\(.*\)"}/\1/')
|
228
|
+
|
229
|
+
# 2. POST login credentials to '/auth/login' (save modified cookies)
|
230
|
+
curl -H "Content-Type: application/json" -X POST \
|
231
|
+
-c cookies.txt -b cookies.txt -d $user \
|
232
|
+
http://localhost:3000/auth/login?code=$client_code
|
233
|
+
|
234
|
+
# 3. GET '/auth/access_token' (save cookies)
|
235
|
+
curl -H "Content-Type: application/json" \
|
236
|
+
-c cookies.txt -b cookies.txt \
|
237
|
+
http://localhost:3000/auth/access_token?code=$client_code
|
238
|
+
#>> {"accessToken":"d09pSG5jVkhFMlVLendBNTdLd1lFZzBjZk5TZE1ONktNcDFiQzhibUV4eklsNURiRFNTbGg5YVFReElLR21HMVhmRzdYSU4vZUxjKzA5OGRjYjFMejJHTmo1UHF1cU00T0ZaNTNWMWVuR2M9LS1FT2RNUkJRbHlaTXU2ZTNvVnAwbGZRPT0=--c0a26b65e91137c82a5a42bcb9fd32f29bdfd0f3","tokenType":"Bearer","expiresIn":1438821498}
|
239
|
+
```
|
240
|
+
|
241
|
+
#### Authorization using ruby `rest-client`
|
242
|
+
```ruby
|
243
|
+
require 'rest-client'
|
244
|
+
triannon_auth = RestClient::Resource.new(
|
245
|
+
'http://localhost:3000/auth',
|
246
|
+
cookies: {},
|
247
|
+
headers: { accept: :json, content_type: :json }
|
248
|
+
)
|
249
|
+
# 1. Obtain a client authorization code (short-lived token)
|
250
|
+
client = { clientId: 'clientA', clientSecret: 'secretA' }
|
251
|
+
response = triannon_auth["/client_identity"].post client.to_json
|
252
|
+
triannon_auth.options[:cookies] = response.cookies # save the cookie data
|
253
|
+
auth = JSON.parse(response.body)
|
254
|
+
client_code = auth['authorizationCode']
|
255
|
+
client_param = "?code=#{client_code}"
|
256
|
+
|
257
|
+
# 2. The client POSTs user credentials
|
258
|
+
user = { userId: 'userA', workgroups: 'org:wg-A' }
|
259
|
+
response = triannon_auth["/login#{client_param}"].post user.to_json
|
260
|
+
triannon_auth.options[:cookies] = response.cookies # save the cookie data
|
261
|
+
|
262
|
+
# 3. The client, on behalf of user, obtains a long-lived access token.
|
263
|
+
response = triannon_auth["/access_token#{client_param}"].get # no content type
|
264
|
+
triannon_auth.options[:cookies] = response.cookies # save the cookie data
|
265
|
+
access = JSON.parse(response.body)
|
266
|
+
access_token = "Bearer #{access['accessToken']}"
|
267
|
+
triannon_auth.headers[:Authorization] = access_token
|
268
|
+
```
|
162
269
|
|
270
|
+
See also the `authenticate` method in:
|
271
|
+
- https://github.com/sul-dlss/triannon-client/blob/master/lib/triannon-client/triannon_client.rb
|
163
272
|
|
164
273
|
# Running This Code in Development
|
165
274
|
|
@@ -6,7 +6,7 @@ module Triannon
|
|
6
6
|
#--- Authentication methods
|
7
7
|
#
|
8
8
|
# The #access_token_data method is generally available to the
|
9
|
-
# application. The #access_token_generate and #
|
9
|
+
# application. The #access_token_generate and #access_token_valid?
|
10
10
|
# methods are consolidated here to provide a unified view of these methods,
|
11
11
|
# although the application generally may not need to call them. They are
|
12
12
|
# used specifically in the auth_controller and these methods are tested
|
@@ -20,37 +20,80 @@ module Triannon
|
|
20
20
|
key = ActiveSupport::KeyGenerator.new(timestamp).generate_key(salt)
|
21
21
|
crypt = ActiveSupport::MessageEncryptor.new(key)
|
22
22
|
session[:access_data] = [timestamp, salt]
|
23
|
-
session[:access_token] = crypt.encrypt_and_sign(
|
23
|
+
session[:access_token] = crypt.encrypt_and_sign(data)
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
# Extract access login data for a session.
|
27
|
+
# @return login_data [Hash] contains login data (empty on failure)
|
28
|
+
def access_token_data
|
29
|
+
@access_data ||= begin
|
30
|
+
data = {}
|
31
|
+
auth = request.headers['Authorization']
|
32
|
+
if auth.nil? || auth !~ /^Bearer/ || session[:access_token].nil?
|
33
|
+
access_token_error
|
34
|
+
else
|
35
|
+
token = auth.split.last
|
36
|
+
if token == session[:access_token]
|
37
|
+
timestamp, salt = session[:access_data]
|
38
|
+
if access_token_expired?(timestamp)
|
39
|
+
access_token_error('Access token expired')
|
40
|
+
else
|
41
|
+
key = ActiveSupport::KeyGenerator.new(timestamp).generate_key(salt)
|
42
|
+
crypt = ActiveSupport::MessageEncryptor.new(key)
|
43
|
+
data = crypt.decrypt_and_verify(token)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
access_token_error('Access code does not match login session')
|
47
|
+
end
|
36
48
|
end
|
37
|
-
|
38
|
-
|
49
|
+
data
|
50
|
+
rescue
|
51
|
+
access_token_error('Failed to validate access code')
|
52
|
+
data
|
39
53
|
end
|
40
54
|
end
|
41
55
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
56
|
+
def access_token_expired?(timestamp)
|
57
|
+
elapsed = Time.now.to_i - timestamp.to_i # sec since code was issued
|
58
|
+
elapsed >= Triannon.config[:access_token_expiry]
|
59
|
+
end
|
60
|
+
|
61
|
+
# decrypt, parse and validate access token
|
62
|
+
def access_token_valid?
|
63
|
+
not access_token_data.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Issue an access token error
|
67
|
+
def access_token_error(msg=nil, status=401)
|
68
|
+
msg ||= 'Access token required'
|
69
|
+
err = {
|
70
|
+
error: 'invalidRequest',
|
71
|
+
errorDescription: msg,
|
72
|
+
errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html#access-token-service'
|
73
|
+
}
|
74
|
+
json_response(err, status)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
# --------------------------------------------------------------------
|
80
|
+
# Utility methods
|
81
|
+
|
82
|
+
# @param data [Hash] Hash.to_json is rendered
|
83
|
+
# @param status [Integer] HTTP status code
|
84
|
+
def json_response(data, status)
|
85
|
+
render json: data.to_json, content_type: json_type_accepted, status: status
|
86
|
+
end
|
87
|
+
|
88
|
+
# Response content type to match an HTTP accept type for JSON formats
|
89
|
+
def json_type_accepted
|
90
|
+
mime_type_from_accept(['application/json', 'text/x-json', 'application/jsonrequest'])
|
51
91
|
end
|
52
92
|
|
53
93
|
|
94
|
+
# --------------------------------------------------------------------
|
95
|
+
# Private methods
|
96
|
+
|
54
97
|
private
|
55
98
|
|
56
99
|
def authorize
|
@@ -68,15 +111,13 @@ module Triannon
|
|
68
111
|
# If the request does not map to a configured container, allow access.
|
69
112
|
container_auth = container_authorization
|
70
113
|
return true if container_auth.empty?
|
71
|
-
|
72
|
-
render401
|
73
|
-
return false
|
74
|
-
end
|
114
|
+
return false unless access_token_valid?
|
75
115
|
# Identify an intersection of the user and the authorized workgroups.
|
76
116
|
container_groups = container_auth['workgroups'] || []
|
77
117
|
match = container_groups & user_workgroups
|
78
118
|
if match.empty?
|
79
|
-
|
119
|
+
msg = 'Write access is denied on this annotation container.'
|
120
|
+
access_token_error(msg, 403)
|
80
121
|
false
|
81
122
|
else
|
82
123
|
true
|
@@ -86,7 +127,8 @@ module Triannon
|
|
86
127
|
# Extract container authorization from the configuration parameters
|
87
128
|
# @return authorization [Hash]
|
88
129
|
def container_authorization
|
89
|
-
|
130
|
+
configs = Triannon.config[:ldp]['anno_containers']
|
131
|
+
container_config = configs[params['anno_root']]
|
90
132
|
if container_config.instance_of? Hash
|
91
133
|
container_config['auth'] || {}
|
92
134
|
else
|
@@ -94,38 +136,11 @@ module Triannon
|
|
94
136
|
end
|
95
137
|
end
|
96
138
|
|
97
|
-
# Map the request root container to config parameters;
|
98
|
-
# assume that a request can only map to one root container.
|
99
|
-
# If this mapping fails, assume that authorization is OK.
|
100
|
-
def request_container_config
|
101
|
-
# TODO: refine the matching algorithm, esp if there is more than one
|
102
|
-
# match rather than assume it can only match one container.
|
103
|
-
request_container = params['anno_root']
|
104
|
-
configs = Triannon.config[:ldp]['anno_containers']
|
105
|
-
configs[request_container]
|
106
|
-
end
|
107
|
-
|
108
139
|
# Extract user workgroups from the access token
|
109
140
|
# @return workgroups [Array<String>]
|
110
141
|
def user_workgroups
|
111
|
-
access_data = access_token_data
|
112
|
-
|
113
|
-
access_data['workgroups'] || []
|
114
|
-
else
|
115
|
-
[]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def render401
|
120
|
-
respond_to do |format|
|
121
|
-
format.all { head :unauthorized }
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def render403
|
126
|
-
respond_to do |format|
|
127
|
-
format.all { head :forbidden }
|
128
|
-
end
|
142
|
+
access_data = access_token_data
|
143
|
+
access_data['workgroups'] || []
|
129
144
|
end
|
130
145
|
|
131
146
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require_dependency "triannon/application_controller"
|
2
2
|
|
3
3
|
module Triannon
|
4
|
-
# Adapted from http://
|
4
|
+
# Adapted from http://iiif.io/api/auth
|
5
5
|
class AuthController < ApplicationController
|
6
6
|
include RdfResponseFormats
|
7
7
|
|
8
|
+
IIIF_AUTH = 'http://iiif.io/api/auth/'
|
9
|
+
|
8
10
|
# HTTP request methods accepted by /auth/login
|
9
11
|
# TODO: enable GET when triannon supports true user authentication
|
10
12
|
LOGIN_ACCEPT = 'OPTIONS, POST'
|
@@ -14,13 +16,7 @@ module Triannon
|
|
14
16
|
# The request MUST use HTTP OPTIONS
|
15
17
|
case request.request_method
|
16
18
|
when 'OPTIONS'
|
17
|
-
|
18
|
-
info = service_info_logout
|
19
|
-
else
|
20
|
-
info = service_info_login
|
21
|
-
end
|
22
|
-
# TODO: include optional info, such as service_info_client_identity
|
23
|
-
json_response(info, 200)
|
19
|
+
json_response(service_info, 200)
|
24
20
|
else
|
25
21
|
# The routes should prevent any execution here.
|
26
22
|
request_method_error(LOGIN_ACCEPT)
|
@@ -28,7 +24,7 @@ module Triannon
|
|
28
24
|
end
|
29
25
|
|
30
26
|
# POST to /auth/login
|
31
|
-
# http://
|
27
|
+
# http://iiif.io/api/auth#login-service
|
32
28
|
def login
|
33
29
|
# The service must set a Cookie for the Access Token Service to retrieve
|
34
30
|
# to determine the user information provided by the authentication system.
|
@@ -42,7 +38,7 @@ module Triannon
|
|
42
38
|
end
|
43
39
|
|
44
40
|
# GET /auth/logout
|
45
|
-
# http://
|
41
|
+
# http://iiif.io/api/auth#logout-service
|
46
42
|
def logout
|
47
43
|
case request.request_method
|
48
44
|
when 'GET'
|
@@ -58,8 +54,8 @@ module Triannon
|
|
58
54
|
# POST /auth/client_identity
|
59
55
|
# A request MUST carry a body with:
|
60
56
|
# { "clientId" : "ID", "clientSecret" : "SECRET" }
|
61
|
-
# http://
|
62
|
-
# http://
|
57
|
+
# http://iiif.io/api/auth#client-identity-service
|
58
|
+
# http://iiif.io/api/auth#error-conditions
|
63
59
|
# return json body [String] containing: { "authorizationCode": code }
|
64
60
|
def client_identity
|
65
61
|
return unless process_post?
|
@@ -76,16 +72,16 @@ module Triannon
|
|
76
72
|
else
|
77
73
|
err = {
|
78
74
|
error: 'invalidClient',
|
79
|
-
errorDescription: '
|
80
|
-
errorUri:
|
75
|
+
errorDescription: 'Unknown client credentials',
|
76
|
+
errorUri: IIIF_AUTH
|
81
77
|
}
|
82
|
-
json_response(err,
|
78
|
+
json_response(err, 403)
|
83
79
|
end
|
84
80
|
else
|
85
81
|
err = {
|
86
|
-
error: '
|
87
|
-
errorDescription: '
|
88
|
-
errorUri:
|
82
|
+
error: 'missingCredentials',
|
83
|
+
errorDescription: 'Requires {"clientId": x, "clientSecret": x}',
|
84
|
+
errorUri: IIIF_AUTH
|
89
85
|
}
|
90
86
|
json_response(err, 401)
|
91
87
|
end
|
@@ -93,8 +89,8 @@ module Triannon
|
|
93
89
|
|
94
90
|
|
95
91
|
# GET /auth/access_token
|
96
|
-
# http://
|
97
|
-
# http://
|
92
|
+
# http://iiif.io/api/auth#access-token-service
|
93
|
+
# http://iiif.io/api/auth#error-conditions
|
98
94
|
def access_token
|
99
95
|
# The cookie established via the login service must be passed to this
|
100
96
|
# service. The service should delete the cookie from the login service
|
@@ -104,13 +100,7 @@ module Triannon
|
|
104
100
|
# When an authorization code was obtained using /auth/client_identity,
|
105
101
|
# that code must be passed to the Access Token Service as well.
|
106
102
|
auth_code = params[:code]
|
107
|
-
if auth_code
|
108
|
-
auth_code_required
|
109
|
-
elsif auth_code_valid?(auth_code)
|
110
|
-
access_token_granted
|
111
|
-
else
|
112
|
-
auth_code_invalid
|
113
|
-
end
|
103
|
+
access_token_granted if auth_code_valid?(auth_code)
|
114
104
|
else
|
115
105
|
# Without an authentication code, a login session is sufficient for
|
116
106
|
# granting an access token. However, the only way to enable a login
|
@@ -128,17 +118,9 @@ module Triannon
|
|
128
118
|
# GET /auth/access_validate
|
129
119
|
# Authorize access based on validating an access token
|
130
120
|
def access_validate
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
else
|
135
|
-
token = auth.split[1]
|
136
|
-
if access_token_valid?(token)
|
137
|
-
response.status = 200
|
138
|
-
render nothing: true
|
139
|
-
else
|
140
|
-
access_token_invalid
|
141
|
-
end
|
121
|
+
if access_token_valid?
|
122
|
+
response.status = 200
|
123
|
+
render nothing: true
|
142
124
|
end
|
143
125
|
end
|
144
126
|
|
@@ -155,22 +137,11 @@ module Triannon
|
|
155
137
|
data = {
|
156
138
|
accessToken: session[:access_token],
|
157
139
|
tokenType: 'Bearer',
|
158
|
-
expiresIn: Triannon.config[:access_token_expiry]
|
140
|
+
expiresIn: Time.now.to_i + Triannon.config[:access_token_expiry]
|
159
141
|
}
|
160
142
|
json_response(data, 200)
|
161
143
|
end
|
162
144
|
|
163
|
-
# Issue a 401 to challenge for a client access token
|
164
|
-
def access_token_invalid
|
165
|
-
err = {
|
166
|
-
error: 'invalidAccess',
|
167
|
-
errorDescription: 'invalid access token',
|
168
|
-
errorUri: ''
|
169
|
-
}
|
170
|
-
json_response(err, 401)
|
171
|
-
end
|
172
|
-
|
173
|
-
|
174
145
|
# --------------------------------------------------------------------
|
175
146
|
# User authentication
|
176
147
|
|
@@ -185,9 +156,7 @@ module Triannon
|
|
185
156
|
return unless process_post?
|
186
157
|
return unless process_json?
|
187
158
|
auth_code = params[:code]
|
188
|
-
if auth_code
|
189
|
-
auth_code_required
|
190
|
-
elsif auth_code_valid?(auth_code)
|
159
|
+
if auth_code_valid?(auth_code)
|
191
160
|
begin
|
192
161
|
data = JSON.parse(request.body.read)
|
193
162
|
required_fields = ['userId', 'workgroups']
|
@@ -204,35 +173,34 @@ module Triannon
|
|
204
173
|
identity.to_json # check JSON compatibility
|
205
174
|
cookies[:login_user] = identity['userId']
|
206
175
|
session[:login_data] = identity
|
207
|
-
|
176
|
+
login_successful
|
208
177
|
else
|
209
178
|
login_required
|
210
179
|
end
|
211
180
|
rescue
|
212
181
|
login_required(422)
|
213
182
|
end
|
214
|
-
else
|
215
|
-
auth_code_invalid
|
216
183
|
end
|
217
184
|
end
|
218
185
|
|
186
|
+
def login_successful
|
187
|
+
render nothing: true, status: 200
|
188
|
+
end
|
189
|
+
|
219
190
|
def login_required(status=401)
|
220
|
-
if request.format == :
|
221
|
-
request_http_basic_authentication
|
222
|
-
elsif request.format == :json
|
223
|
-
# response.headers["WWW-Authenticate"] = %(Basic realm="Application")
|
191
|
+
if request.format == :json
|
224
192
|
if status == 401
|
225
193
|
err = {
|
226
|
-
error: '
|
194
|
+
error: 'missingCredentials',
|
227
195
|
errorDescription: 'login credentials required',
|
228
|
-
errorUri:
|
196
|
+
errorUri: IIIF_AUTH
|
229
197
|
}
|
230
198
|
end
|
231
199
|
if status == 422
|
232
200
|
err = {
|
233
|
-
error: '
|
201
|
+
error: 'invalidRequest',
|
234
202
|
errorDescription: 'login credentials cannot be parsed',
|
235
|
-
errorUri:
|
203
|
+
errorUri: IIIF_AUTH
|
236
204
|
}
|
237
205
|
end
|
238
206
|
json_response(err, status)
|
@@ -266,41 +234,43 @@ module Triannon
|
|
266
234
|
|
267
235
|
# decrypt, parse and validate authorization code
|
268
236
|
def auth_code_valid?(code)
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
237
|
+
if code.nil? || session[:client_token].nil?
|
238
|
+
auth_code_error
|
239
|
+
else
|
240
|
+
begin
|
241
|
+
if code == session[:client_token]
|
242
|
+
identity, salt = session[:client_data]
|
243
|
+
key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
|
244
|
+
crypt = ActiveSupport::MessageEncryptor.new(key)
|
245
|
+
data, timestamp = crypt.decrypt_and_verify(code)
|
246
|
+
elapsed = Time.now.to_i - timestamp.to_i # sec since code issued
|
247
|
+
if elapsed < Triannon.config[:client_token_expiry]
|
248
|
+
return data
|
249
|
+
else
|
250
|
+
auth_code_error('Authorization code expired')
|
251
|
+
end
|
252
|
+
else
|
253
|
+
msg = 'Unable to validate authorization code'
|
254
|
+
auth_code_error(msg)
|
255
|
+
end
|
256
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
257
|
+
# This is an invalid code, so return false.
|
277
258
|
end
|
278
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
279
|
-
# This is an invalid code, so return nil (a falsy value).
|
280
259
|
end
|
260
|
+
false
|
281
261
|
end
|
282
262
|
|
283
|
-
# Issue a
|
284
|
-
def
|
263
|
+
# Issue a client authorization error
|
264
|
+
def auth_code_error(msg=nil, status=401)
|
265
|
+
msg ||= 'Client authorization required'
|
285
266
|
err = {
|
286
267
|
error: 'invalidClient',
|
287
|
-
errorDescription:
|
288
|
-
errorUri: ''
|
268
|
+
errorDescription: msg,
|
269
|
+
errorUri: 'http://iiif.io/api/auth#client-identity-service'
|
289
270
|
}
|
290
|
-
json_response(err,
|
271
|
+
json_response(err, status)
|
291
272
|
end
|
292
273
|
|
293
|
-
# Issue a 401 to challenge for a client authorization code
|
294
|
-
def auth_code_required
|
295
|
-
err = {
|
296
|
-
error: 'invalidClient',
|
297
|
-
errorDescription: 'authorization code is required',
|
298
|
-
errorUri: ''
|
299
|
-
}
|
300
|
-
json_response(err, 401)
|
301
|
-
end
|
302
|
-
|
303
|
-
|
304
274
|
# --------------------------------------------------------------------
|
305
275
|
# Service information data
|
306
276
|
# TODO: evaluate whether we need this data
|
@@ -311,67 +281,58 @@ module Triannon
|
|
311
281
|
uri.to_s.sub(uri.path,'')
|
312
282
|
end
|
313
283
|
|
314
|
-
# http://
|
315
|
-
# return info [Hash]
|
316
|
-
def
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
}
|
324
|
-
end
|
325
|
-
|
326
|
-
# http://image-auth.iiif.io/api/image/2.1/authentication.html#client-identity-service
|
327
|
-
# return info [Hash] client identity service information
|
328
|
-
def service_info_client_identity
|
329
|
-
{
|
330
|
-
service: {
|
331
|
-
"@id" => service_base_uri + '/auth/client_identity',
|
332
|
-
"profile" => "http://iiif.io/api/image/2/auth/clientId"
|
333
|
-
}
|
334
|
-
}
|
284
|
+
# http://iiif.io/api/auth#login-service
|
285
|
+
# return info [Hash] authorization service information
|
286
|
+
def service_info
|
287
|
+
info = service_info_login
|
288
|
+
services = info['service']['service']
|
289
|
+
services.push service_info_logout
|
290
|
+
services.push service_info_client_identity
|
291
|
+
services.push service_info_access_token
|
292
|
+
info
|
335
293
|
end
|
336
294
|
|
337
|
-
# http://
|
295
|
+
# http://iiif.io/api/auth#login-service
|
338
296
|
# return info [Hash] login service information
|
339
297
|
def service_info_login
|
340
298
|
{
|
341
|
-
service
|
342
|
-
|
343
|
-
|
344
|
-
|
299
|
+
'service' => {
|
300
|
+
'@id' => service_base_uri + '/auth/login',
|
301
|
+
'profile' => 'http://iiif.io/api/auth/0/login',
|
302
|
+
'label' => 'Login to Triannon',
|
303
|
+
'service' => [
|
304
|
+
# Related services ...
|
305
|
+
]
|
345
306
|
}
|
346
307
|
}
|
347
308
|
end
|
348
309
|
|
349
|
-
# http://
|
310
|
+
# http://iiif.io/api/auth#logout-service
|
350
311
|
# return info [Hash] logout service information
|
351
312
|
def service_info_logout
|
352
313
|
{
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
"label" => "Logout of Triannon"
|
357
|
-
}
|
314
|
+
"@id" => service_base_uri + '/auth/logout',
|
315
|
+
"profile" => "http://iiif.io/api/auth/0/logout",
|
316
|
+
"label" => "Logout of Triannon"
|
358
317
|
}
|
359
318
|
end
|
360
319
|
|
361
|
-
|
362
|
-
#
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
def json_response(data, status)
|
369
|
-
render json: data.to_json, content_type: json_type_accepted, status: status
|
320
|
+
# http://iiif.io/api/auth#access-token-service
|
321
|
+
# return info [Hash] access token service information
|
322
|
+
def service_info_access_token
|
323
|
+
{
|
324
|
+
"@id" => service_base_uri + '/auth/access_token',
|
325
|
+
"profile" => "http://iiif.io/api/auth/0/token",
|
326
|
+
}
|
370
327
|
end
|
371
328
|
|
372
|
-
#
|
373
|
-
|
374
|
-
|
329
|
+
# http://iiif.io/api/auth#client-identity-service
|
330
|
+
# return info [Hash] client identity service information
|
331
|
+
def service_info_client_identity
|
332
|
+
{
|
333
|
+
"@id" => service_base_uri + '/auth/client_identity',
|
334
|
+
"profile" => "http://iiif.io/api/auth/0/clientId",
|
335
|
+
}
|
375
336
|
end
|
376
337
|
|
377
338
|
# Parse POST JSON data to ensure it contains required fields
|
@@ -400,14 +361,7 @@ module Triannon
|
|
400
361
|
if request.post?
|
401
362
|
true
|
402
363
|
else
|
403
|
-
|
404
|
-
err = {
|
405
|
-
error: 'invalidRequest',
|
406
|
-
errorDescription: "#{request.path} accepts POST requests, not #{request.request_method}",
|
407
|
-
errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
|
408
|
-
}
|
409
|
-
response.headers.merge!({'Allow' => 'POST'})
|
410
|
-
json_response(err, 405)
|
364
|
+
request_method_error('POST')
|
411
365
|
false
|
412
366
|
end
|
413
367
|
end
|
@@ -416,19 +370,14 @@ module Triannon
|
|
416
370
|
def request_method_error(accept)
|
417
371
|
logger.debug "Rejected Request Method: #{request.request_method}"
|
418
372
|
response.status = 405
|
419
|
-
response.headers.merge!(Allow
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
|
426
|
-
}
|
427
|
-
render json: err.to_json, content_type: json_type_accepted
|
428
|
-
}
|
429
|
-
format.html {
|
430
|
-
render nothing: true
|
373
|
+
response.headers.merge!({'Allow' => accept})
|
374
|
+
if request.format == :json
|
375
|
+
err = {
|
376
|
+
error: 'invalidRequest',
|
377
|
+
errorDescription: "#{request.path} accepts: #{accept}",
|
378
|
+
errorUri: IIIF_AUTH
|
431
379
|
}
|
380
|
+
render json: err.to_json, content_type: json_type_accepted
|
432
381
|
end
|
433
382
|
end
|
434
383
|
|
data/config/triannon.yml
CHANGED
@@ -31,7 +31,7 @@ development:
|
|
31
31
|
clientA: secretA
|
32
32
|
clientB: secretB
|
33
33
|
# expiry values are in seconds
|
34
|
-
client_token_expiry:
|
34
|
+
client_token_expiry: 120
|
35
35
|
access_token_expiry: 3600
|
36
36
|
|
37
37
|
test: &test
|
@@ -52,7 +52,7 @@ test: &test
|
|
52
52
|
clientA: secretA
|
53
53
|
clientB: secretB
|
54
54
|
# expiry values are in seconds
|
55
|
-
client_token_expiry:
|
55
|
+
client_token_expiry: 120
|
56
56
|
access_token_expiry: 3600
|
57
57
|
|
58
58
|
production:
|
@@ -71,5 +71,5 @@ production:
|
|
71
71
|
triannon_base_url: http://your.triannon-server.com/annotations/
|
72
72
|
authorized_clients:
|
73
73
|
# expiry values are in seconds
|
74
|
-
client_token_expiry:
|
74
|
+
client_token_expiry: 120
|
75
75
|
access_token_expiry: 3600
|
@@ -36,7 +36,7 @@ development:
|
|
36
36
|
clientA: secretA
|
37
37
|
clientB: secretB
|
38
38
|
# expiry values are in seconds
|
39
|
-
client_token_expiry:
|
39
|
+
client_token_expiry: 120
|
40
40
|
access_token_expiry: 3600
|
41
41
|
test: &test
|
42
42
|
ldp:
|
@@ -56,22 +56,19 @@ test: &test
|
|
56
56
|
clientA: secretA
|
57
57
|
clientB: secretB
|
58
58
|
# expiry values are in seconds
|
59
|
-
client_token_expiry:
|
59
|
+
client_token_expiry: 120
|
60
60
|
access_token_expiry: 3600
|
61
61
|
production:
|
62
62
|
ldp:
|
63
63
|
url:
|
64
64
|
uber_container: anno
|
65
65
|
anno_containers:
|
66
|
-
foo:
|
67
|
-
bar:
|
68
|
-
auth:
|
69
|
-
users: []
|
70
|
-
workgroups:
|
71
|
-
- org:wg-A
|
72
|
-
- org:wg-B
|
73
66
|
solr_url:
|
74
67
|
triannon_base_url:
|
68
|
+
authorized_clients:
|
69
|
+
# expiry values are in seconds
|
70
|
+
client_token_expiry: 120
|
71
|
+
access_token_expiry: 3600
|
75
72
|
YML
|
76
73
|
create_file 'config/triannon.yml', default_yml
|
77
74
|
end
|
data/lib/triannon/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: triannon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Naomi Dushay
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2015-
|
13
|
+
date: 2015-08-06 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|