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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bbca5b2eeb46df56a16905386aa0896e32edc1d
4
- data.tar.gz: 4057efb53bd40de5dd20a637b0e6eeaa581e3274
3
+ metadata.gz: 051a29bfe3ab0af248eae2e88c87a597d5e2f99d
4
+ data.tar.gz: eb636792129fe4969f33f5dd2f86248b4b758608
5
5
  SHA512:
6
- metadata.gz: ade73d1212e2d5ef3fb815946279af50e58a018999ebb0b1c7d8c8db9a5f56d85ddb4a42d9472d5c142e2ea1e1079646889fac21859afe61370ca47c85dae552
7
- data.tar.gz: db831b37c53215465bb6441ea599dca56722c467a9968c477e7b2a0780d2070c7d8d2b717be6b645a1bff23d1c5a0ceb70578294c05a5535e5d3dd11057b2e22
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 specific anno containers
32
- * `anno_containers:` (the names of LDP Basic Containers holding individual annos)
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
- Generate the root annotations containers on your LDP server:
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
- Set up caching for jsonld context documents:
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
- ### Get a list of annos
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
- ### Get a list of annos in a particular root container
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
- ### Get a particular anno
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
- ### Create an anno
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
- ### Delete an anno
194
+ #### Delete an anno
159
195
  `DELETE`: `http://(host)/annotations/(root container)/(anno_id)`
160
196
 
161
- 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")
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 #access_token_validate?
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([data, timestamp])
23
+ session[:access_token] = crypt.encrypt_and_sign(data)
24
24
  end
25
25
 
26
- # decrypt, parse and validate access token
27
- def access_token_valid?(code)
28
- begin
29
- if code == session[:access_token]
30
- identity, salt = session[:access_data]
31
- key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
32
- crypt = ActiveSupport::MessageEncryptor.new(key)
33
- data, timestamp = crypt.decrypt_and_verify(code)
34
- elapsed = Time.now.to_i - timestamp.to_i # sec since token was issued
35
- return data if elapsed < Triannon.config[:access_token_expiry]
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
- rescue ActiveSupport::MessageVerifier::InvalidSignature
38
- # This is an invalid code, so return nil (a falsy value).
49
+ data
50
+ rescue
51
+ access_token_error('Failed to validate access code')
52
+ data
39
53
  end
40
54
  end
41
55
 
42
- # Extract access login data from Authorization header, if it is valid.
43
- # @param headers [Hash] request.headers with 'Authorization'
44
- # @return login_data [Hash|nil]
45
- def access_token_data(headers)
46
- auth = headers['Authorization']
47
- unless auth.nil? || auth !~ /^Bearer/
48
- token = auth.split.last
49
- access_token_valid?(token)
50
- end
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
- if request.headers['Authorization'].nil?
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
- render403
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
- container_config = request_container_config
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(request.headers)
112
- if access_data.instance_of? Hash
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://image-auth.iiif.io/api/image/2.1/authentication.html
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
- if cookies[:login_user]
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://image-auth.iiif.io/api/image/2.1/authentication.html#login-service
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://image-auth.iiif.io/api/image/2.1/authentication.html#logout-service
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://image-auth.iiif.io/api/image/2.1/authentication.html#client-identity-service
62
- # http://image-auth.iiif.io/api/image/2.1/authentication.html#error-conditions
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: 'Invalid client credentials',
80
- errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
75
+ errorDescription: 'Unknown client credentials',
76
+ errorUri: IIIF_AUTH
81
77
  }
82
- json_response(err, 401)
78
+ json_response(err, 403)
83
79
  end
84
80
  else
85
81
  err = {
86
- error: 'invalidClient',
87
- errorDescription: 'Insufficient client data for authentication',
88
- errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
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://image-auth.iiif.io/api/image/2.1/authentication.html#access-token-service
97
- # http://image-auth.iiif.io/api/image/2.1/authentication.html#error-conditions
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.nil?
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
- auth = request.headers['Authorization']
132
- if auth.nil? || auth !~ /Bearer/
133
- access_token_invalid
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.nil?
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
- redirect_to root_url, notice: 'Successfully logged in.'
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 == :html
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: '401 Unauthorized',
194
+ error: 'missingCredentials',
227
195
  errorDescription: 'login credentials required',
228
- errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
196
+ errorUri: IIIF_AUTH
229
197
  }
230
198
  end
231
199
  if status == 422
232
200
  err = {
233
- error: '422 Unprocessable Entity',
201
+ error: 'invalidRequest',
234
202
  errorDescription: 'login credentials cannot be parsed',
235
- errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
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
- begin
270
- if code == session[:client_token]
271
- identity, salt = session[:client_data]
272
- key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
273
- crypt = ActiveSupport::MessageEncryptor.new(key)
274
- data, timestamp = crypt.decrypt_and_verify(code)
275
- elapsed = Time.now.to_i - timestamp.to_i # sec since code was issued
276
- return data if elapsed < Triannon.config[:client_token_expiry]
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 403 for invalid client authorization codes
284
- def auth_code_invalid
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: 'Unable to validate authorization code',
288
- errorUri: ''
268
+ errorDescription: msg,
269
+ errorUri: 'http://iiif.io/api/auth#client-identity-service'
289
270
  }
290
- json_response(err, 403)
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://image-auth.iiif.io/api/image/2.1/authentication.html#access-token-service
315
- # return info [Hash] access token service information
316
- def service_info_access_token
317
- {
318
- service: {
319
- "@id" => service_base_uri + '/auth/access_token',
320
- "profile" => "http://iiif.io/api/image/2/auth/token",
321
- "label" => "Request Access Token for Triannon"
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://image-auth.iiif.io/api/image/2.1/authentication.html#login-service
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
- "@id" => service_base_uri + '/auth/login',
343
- "profile" => "http://iiif.io/api/image/2/auth/login",
344
- "label" => "Login to Triannon"
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://image-auth.iiif.io/api/image/2.1/authentication.html#logout-service
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
- service: {
354
- "@id" => service_base_uri + '/auth/logout',
355
- "profile" => "http://iiif.io/api/image/2/auth/logout",
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
- # Utility methods
364
-
365
-
366
- # @param data [Hash] Hash.to_json is rendered
367
- # @param status [Integer] HTTP status code
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
- # Response content type to match an HTTP accept type for JSON formats
373
- def json_type_accepted
374
- mime_type_from_accept(['application/json', 'text/x-json', 'application/jsonrequest'])
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
- logger.debug "Rejected Request Method: #{request.request_method}"
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: accept)
420
- respond_to do |format|
421
- format.json {
422
- err = {
423
- error: 'invalidRequest',
424
- errorDescription: "#{request.path} accepts: #{accept}",
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
 
@@ -31,7 +31,7 @@ development:
31
31
  clientA: secretA
32
32
  clientB: secretB
33
33
  # expiry values are in seconds
34
- client_token_expiry: 60
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: 60
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: 60
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: 60
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: 60
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
@@ -1,3 +1,3 @@
1
1
  module Triannon
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.0"
3
3
  end
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.0.0
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-07-22 00:00:00.000000000 Z
13
+ date: 2015-08-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails