wix-apps 0.0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -1
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -6
- data/Gemfile +0 -2
- data/Guardfile +1 -1
- data/README.md +64 -8
- data/Rakefile +7 -3
- data/lib/wix-apps.rb +1 -2
- data/lib/wix-apps/signed_instance.rb +85 -34
- data/lib/wix-apps/signed_instance_middleware.rb +48 -42
- data/lib/wix-apps/version.rb +1 -1
- data/spec/lib/wix-apps/signed_instance_middleware_spec.rb +133 -67
- data/spec/lib/wix-apps/signed_instance_spec.rb +120 -55
- data/spec/spec_helper.rb +42 -1
- data/wix-apps.gemspec +18 -14
- metadata +69 -49
- data/.rvmrc +0 -52
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a314236e1cdbd6379c6bf7b4a9731508f1f29751
|
4
|
+
data.tar.gz: a8e6f86a69f82c7c36deef8aa4efb8d2a923d4d8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 222b61a2e958fecd8e2e0eb685a4a17e15601b476c067311f436251292aa876ba5f134eab6509f1ef015e2b45bb3008f5612d18419a5bebfe1f8c18ec4a7ed88
|
7
|
+
data.tar.gz: 585213545bda5d1786852813c2a55d8c9758effbcb7f91210d84c9ee413eba214ca1be50d0ea277a2b3c667cb3bcac35d395c5c6e9635a73d213ee38ae81d80a
|
data/.rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
--color
|
2
|
-
--format documentation
|
2
|
+
--format documentation
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
wix-apps
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/.travis.yml
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
+
before_script:
|
2
|
+
- uname -a
|
3
|
+
before_install:
|
4
|
+
- gem install bundler
|
1
5
|
language: ruby
|
2
6
|
rvm:
|
3
|
-
-
|
4
|
-
- 1
|
5
|
-
-
|
6
|
-
-
|
7
|
+
- 2.0
|
8
|
+
- 2.1
|
9
|
+
- 2.2
|
10
|
+
- 2.3
|
7
11
|
- ruby-head
|
8
12
|
- jruby-head
|
9
|
-
bundler_args: --without development
|
10
13
|
notifications:
|
11
14
|
email:
|
12
|
-
- gregory@wix.com
|
15
|
+
- gregory@wix.com
|
16
|
+
- niklas@bichinger.de
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# Wix::Apps
|
2
2
|
[![Build Status](https://secure.travis-ci.org/wix/wix-apps-ruby.png?branch=master)](http://travis-ci.org/wix/wix-apps-ruby)
|
3
3
|
|
4
|
-
Rack middleware use with "Third Party Applications".
|
5
|
-
It checks signature and passes
|
4
|
+
Rack middleware for use with "Third Party Applications" on the Wix platform.
|
5
|
+
It handles the instance param passed by Wix requests for configurable paths. It checks the signature, parses the contents and passes a convenient Object to your application via env.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
|
+
Important note: if you're using a pre-1.0.0-version of this gem, please be aware the API changed significantly. You will have to update some of your code. However, in versions >= 1.0.0 should be a corresponding method for anything from version 0.0.x.
|
10
|
+
|
9
11
|
Add this line to your application's Gemfile:
|
10
12
|
|
11
|
-
gem 'wix-apps'
|
13
|
+
gem 'wix-apps', '~> 1.0.0'
|
12
14
|
|
13
15
|
And then execute:
|
14
16
|
|
@@ -21,17 +23,71 @@ Or install it yourself as:
|
|
21
23
|
## Usage
|
22
24
|
|
23
25
|
### Any Rack Application
|
24
|
-
Add Wix::Apps::SignedInstanceMiddleware
|
26
|
+
Add `Wix::Apps::SignedInstanceMiddleware` like any other middleware.
|
25
27
|
```ruby
|
26
|
-
use Wix::Apps::SignedInstanceMiddleware,
|
28
|
+
use Wix::Apps::SignedInstanceMiddleware, secret_key: 'secret_key',
|
29
|
+
secured_paths: ['/your', '/paths', %r{\A/wix/(auth|update)\z}]
|
27
30
|
```
|
28
31
|
### Rails
|
29
|
-
In application.rb
|
32
|
+
In `application.rb`, add:
|
33
|
+
```ruby
|
34
|
+
config.middleware.use Wix::Apps::SignedInstanceMiddleware, secret_key: 'your-secret-key',
|
35
|
+
secured_paths: ['/wix']
|
36
|
+
```
|
37
|
+
|
38
|
+
In your controller handling a configured secured path, access the signed instance:
|
30
39
|
```ruby
|
31
|
-
|
32
|
-
|
40
|
+
class WixController < ApplicationController
|
41
|
+
def index
|
42
|
+
# retrieve Wix::Apps::SignedInstance object
|
43
|
+
instance = request.env['wix.instance']
|
44
|
+
|
45
|
+
if instance.owner_permissions?
|
46
|
+
...
|
33
47
|
```
|
34
48
|
|
49
|
+
### env['wix.instance']
|
50
|
+
This WixApps middleware passes on a `Wix::Apps::SignedInstance` object through env['wix.instance']. The object has the following methods:
|
51
|
+
|
52
|
+
Object method | Type | Description
|
53
|
+
------------- | ---- | -----------
|
54
|
+
instance_id | String | Required Wix instance property `instanceId`
|
55
|
+
sign_date | DateTime | Required Wix instance property `signDate`
|
56
|
+
uid | String | Optional Wix instance property `uid`
|
57
|
+
permissions | String | Required Wix instance property `permissions`
|
58
|
+
ip_and_port | String | Required Wix instance property `ipAndPort`
|
59
|
+
vendor_product_id | String | Required Wix instance property `vendorProductId`
|
60
|
+
aid | String | Required Wix instance property `aid`
|
61
|
+
origin_instance_id | String | Optional Wix instance property `originInstanceId`
|
62
|
+
site_owner_id | String | Required Wix instance property `siteOwnerId`
|
63
|
+
owner_permissions? | Boolean | Indicates if instance user has owner (includes site administrators) permissions
|
64
|
+
owner_logged_in? | Boolean | Indicates if instance user is the single owner of the site
|
65
|
+
|
66
|
+
### Options
|
67
|
+
|
68
|
+
#### secured_paths
|
69
|
+
An Array of String- and Regexp-objects describing the paths to be secured by checking the Wix signed instance. On request, the path will be checked against every entry in this list. Strings are checked for equality, Regexps are matched (without additional conditions, so use \A and \z for start and end markers if you need them!). Keep in mind rack's paths always begin with a '/'.
|
70
|
+
|
71
|
+
Example (secures '/auth_path', every path containing the string 'wix' and every path starting with '/wox/'):
|
72
|
+
|
73
|
+
secured_paths: ['/auth_path', /wix/, %r{\A/wox/}]
|
74
|
+
|
75
|
+
#### paths
|
76
|
+
Just like secured_paths, except these paths may or may not receive a Wix signed instance. If it does, the instance has to be valid. Calling `request.env('wix.instance')` in the controller may return nil (although the env key `'wix.instance'` exists) if there was no instance passed.
|
77
|
+
|
78
|
+
#### secret_key
|
79
|
+
A String containing your Wix app's secret key.
|
80
|
+
|
81
|
+
Example:
|
82
|
+
|
83
|
+
secret_key: 'd245bbf8-57eb-49d6-aeff-beff6d82cd39'
|
84
|
+
|
85
|
+
#### (optional) strict_properties
|
86
|
+
A Boolean indicating if the instance properties of the Wix signed instance should be checked for completeness (true, default) or not (false). You should never need to set it to false and it's meant for debugging purposes only.
|
87
|
+
|
88
|
+
Example:
|
89
|
+
|
90
|
+
strict_properties: true
|
35
91
|
|
36
92
|
## Contributing
|
37
93
|
|
data/Rakefile
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
|
-
require
|
2
|
+
require 'bundler/gem_tasks'
|
3
3
|
require 'rspec/core/rake_task'
|
4
4
|
|
5
5
|
desc 'Default: run specs.'
|
6
6
|
task :default => :spec
|
7
7
|
|
8
|
-
desc
|
9
|
-
RSpec::Core::RakeTask.new
|
8
|
+
desc 'Run specs'
|
9
|
+
RSpec::Core::RakeTask.new
|
10
|
+
|
11
|
+
task :console do
|
12
|
+
exec 'irb -r wix-apps.rb -I ./lib'
|
13
|
+
end
|
data/lib/wix-apps.rb
CHANGED
@@ -4,62 +4,113 @@ require 'openssl'
|
|
4
4
|
|
5
5
|
module Wix
|
6
6
|
module Apps
|
7
|
-
|
8
|
-
class
|
9
|
-
|
10
|
-
|
7
|
+
|
8
|
+
class SignedInstanceParseError < Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
class SignedInstanceNoSecretKey < Exception
|
12
|
+
end
|
13
|
+
|
14
|
+
# This class deals with Wix Signed Instance
|
15
|
+
# (http://dev.wix.com/docs/display/DRAF/Using+the+Signed+App+Instance)
|
11
16
|
#
|
12
17
|
# Example:
|
13
18
|
# si = SignedInstance.new('vrinSv2HB9tqbnJ....')
|
14
19
|
class SignedInstance
|
15
|
-
attr_reader :raw_signed_instance, :instance_id, :sign_date, :uid,
|
16
|
-
:permissions
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
# maps required instance properties to object attributes
|
22
|
+
REQUIRED_PROPERTIES = {
|
23
|
+
'instanceId' => :instance_id,
|
24
|
+
'signDate' => :sign_date,
|
25
|
+
'permissions' => :permissions,
|
26
|
+
'ipAndPort' => :ip_and_port,
|
27
|
+
'vendorProductId' => :vendor_product_id,
|
28
|
+
'aid' => :aid,
|
29
|
+
'siteOwnerId' => :site_owner_id,
|
30
|
+
}
|
21
31
|
|
22
|
-
|
23
|
-
|
32
|
+
# maps optional instance properties to object attributes
|
33
|
+
OPTIONAL_PROPERTIES = {
|
34
|
+
'uid' => :uid,
|
35
|
+
'originInstanceId' => :origin_instance_id,
|
36
|
+
}
|
24
37
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
38
|
+
PERMISSIONS_OWNER = 'OWNER'
|
39
|
+
|
40
|
+
attr_reader :raw_signed_instance, :strict_properties
|
41
|
+
attr_reader *(REQUIRED_PROPERTIES.values + OPTIONAL_PROPERTIES.values)
|
42
|
+
|
43
|
+
# @param [String] raw_signed_instance The "instance" parameter Wix sends with the request
|
44
|
+
# @param [Hash] options Options for
|
45
|
+
def initialize(raw_signed_instance, options={})
|
46
|
+
self.strict_properties = options[:strict_properties].nil? ? true : !!options[:strict_properties]
|
47
|
+
self.secret_key = options[:secret_key] || options[:secret] # :secret for backwards compatibility
|
48
|
+
raise SignedInstanceNoSecretKey.new('secret key must be provided') if secret_key.nil?
|
49
|
+
self.raw_signed_instance = raw_signed_instance
|
50
|
+
raise SignedInstanceParseError.new('invalid instance signature') unless instance_signature_valid?
|
31
51
|
|
32
|
-
|
52
|
+
initialize_from_signed_instance
|
33
53
|
end
|
34
54
|
|
35
|
-
#
|
36
|
-
def
|
37
|
-
permissions ==
|
55
|
+
# owner or site collaborator visiting?
|
56
|
+
def owner_permissions?
|
57
|
+
permissions == PERMISSIONS_OWNER
|
58
|
+
end
|
59
|
+
|
60
|
+
# did the one single site owner log in?
|
61
|
+
def owner_logged_in?
|
62
|
+
# note: site owner id is required so we wouldn't have to check for nil,
|
63
|
+
# but this method's output can be very important and I'm paranoid. ;)
|
64
|
+
!site_owner_id.nil? && site_owner_id == uid
|
38
65
|
end
|
39
66
|
|
40
67
|
private
|
41
|
-
|
42
|
-
|
43
|
-
|
68
|
+
|
69
|
+
attr_accessor :secret_key
|
70
|
+
attr_writer :raw_signed_instance, :strict_properties
|
71
|
+
attr_writer *(REQUIRED_PROPERTIES.values + OPTIONAL_PROPERTIES.values)
|
72
|
+
|
73
|
+
# validates signature
|
74
|
+
def instance_signature_valid?
|
75
|
+
signature, encoded_json = (raw_signed_instance || '').split('.', 2)
|
76
|
+
return false if signature.nil? || encoded_json.nil?
|
77
|
+
|
78
|
+
digest = OpenSSL::Digest.new('sha256')
|
79
|
+
hmac_digest = OpenSSL::HMAC.digest(digest, secret_key, encoded_json)
|
80
|
+
my_signature = Base64.urlsafe_encode64(hmac_digest).gsub('=', '')
|
81
|
+
|
82
|
+
my_signature == signature
|
83
|
+
end
|
84
|
+
|
85
|
+
# initializes object attributes from parsed instance
|
86
|
+
def initialize_from_signed_instance
|
87
|
+
encoded_json = raw_signed_instance.split('.', 2).last
|
44
88
|
|
45
89
|
# Need to add Base64 padding.
|
46
90
|
# (http://stackoverflow.com/questions/4987772/decoding-facebooks-signed-request-in-ruby-sinatra)
|
47
|
-
padded_json =
|
48
|
-
padded_json += ('=' * (4 -
|
91
|
+
padded_json = encoded_json
|
92
|
+
padded_json += ('=' * (4 - encoded_json.length % 4)) if padded_json.length % 4 != 0
|
49
93
|
|
50
94
|
begin
|
51
|
-
|
52
|
-
signed_instance = MultiJson.load(
|
53
|
-
rescue ArgumentError, MultiJson::
|
95
|
+
json = Base64.urlsafe_decode64(padded_json)
|
96
|
+
signed_instance = MultiJson.load(json)
|
97
|
+
rescue ArgumentError, MultiJson::ParseError => e
|
54
98
|
raise SignedInstanceParseError.new(e.message)
|
55
99
|
end
|
56
100
|
|
57
|
-
|
58
|
-
|
59
|
-
|
101
|
+
# set all required instance properties
|
102
|
+
REQUIRED_PROPERTIES.each { |instance_key, attribute|
|
103
|
+
instance_value = signed_instance[instance_key]
|
104
|
+
raise SignedInstanceParseError.new("missing instance property: #{instance_key}") if strict_properties && instance_value.nil?
|
105
|
+
send "#{attribute}=", instance_value
|
106
|
+
}
|
107
|
+
# overwrite sign date with real DateTime object
|
108
|
+
self.sign_date = DateTime.parse(sign_date) if strict_properties || sign_date
|
60
109
|
|
61
|
-
|
62
|
-
|
110
|
+
# set all optional instance properties (if set)
|
111
|
+
OPTIONAL_PROPERTIES.each { |instance_key, attribute|
|
112
|
+
send "#{attribute}=", signed_instance[instance_key] if signed_instance.has_key? instance_key
|
113
|
+
}
|
63
114
|
end
|
64
115
|
end
|
65
116
|
end
|
@@ -1,65 +1,71 @@
|
|
1
1
|
module Wix
|
2
2
|
module Apps
|
3
|
-
class SignedInstanceMiddleware < Struct.new :app, :
|
3
|
+
class SignedInstanceMiddleware < Struct.new :app, :secret_key, :secured_paths, :paths
|
4
|
+
|
5
|
+
# Initializes the middleware to secure a given set of paths.
|
6
|
+
# Options:
|
7
|
+
# :secret_key - the Wix secret key as String
|
8
|
+
# :secured_paths (optional) - an Array of String and Regexp objects which every request's path is matched against. Matching paths are required to pass a Wix signed instance.
|
4
9
|
def initialize(app, options={})
|
5
|
-
|
6
|
-
|
10
|
+
self.app = app
|
11
|
+
self.secret_key = options[:secret_key]
|
12
|
+
self.secured_paths = options[:secured_paths] || []
|
13
|
+
self.paths = options[:paths] || []
|
7
14
|
end
|
8
15
|
|
16
|
+
# Checks current URL path for Wix instance requirement, parses given signed instance and adds GET param 'parsed_instance' with the instance's parsed properties.
|
17
|
+
# @param [Hash] env The current environment hash
|
18
|
+
# @return [Array] The typical [<code>, <headers>, <body>] rack response Array
|
9
19
|
def call(env)
|
10
|
-
|
20
|
+
path = env['PATH_INFO']
|
11
21
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@instance = Wix::Apps::SignedInstance.new(@request.params['instance'],
|
17
|
-
secret: options[:secret_key])
|
18
|
-
rescue Wix::Apps::SignedInstanceParseError => e
|
19
|
-
return [403, {}, ['Invalid wix instance']]
|
20
|
-
end
|
22
|
+
secured_path = secured_path? path
|
23
|
+
if secured_path || path?(path)
|
24
|
+
# path must be handled (instance is either required or optional)
|
25
|
+
request = Rack::Request.new(env)
|
21
26
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
[
|
27
|
+
env['wix.instance'] = nil
|
28
|
+
if request.params.has_key? 'instance'
|
29
|
+
# Wix' "instance" parameter was supplied so it must be parseable. parse and set it into env.
|
30
|
+
begin
|
31
|
+
env['wix.instance'] = Wix::Apps::SignedInstance.new(request.params['instance'], secret: secret_key)
|
32
|
+
rescue Wix::Apps::SignedInstanceParseError
|
33
|
+
# 403 Forbidden
|
34
|
+
return [403, {}, ['Invalid Wix instance']]
|
27
35
|
end
|
28
|
-
|
29
|
-
|
36
|
+
elsif secured_path
|
37
|
+
# instance is required but Wix' "instance" parameter is missing
|
38
|
+
# 401 Unauthorized
|
39
|
+
return [401, {}, ['Unauthorized']]
|
30
40
|
end
|
31
|
-
else
|
32
|
-
@app.call(env)
|
33
41
|
end
|
34
|
-
|
42
|
+
app.call(env)
|
35
43
|
end
|
36
44
|
|
37
45
|
private
|
38
|
-
def initialize_options(options={})
|
39
|
-
self.options = {
|
40
|
-
:secret_key => nil,
|
41
|
-
:secured_paths => []
|
42
|
-
}.merge(options)
|
43
|
-
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
# Check if a request URL's path should be required to pass a Wix signed instance or not. Checks the path against the secured_paths option.
|
48
|
+
# @param [String] path URL path (=directory part) to check
|
49
|
+
# @return [Boolean] Indicates if given path should be secured or not
|
50
|
+
def secured_path?(path)
|
51
|
+
path_match? secured_paths, path
|
47
52
|
end
|
48
53
|
|
49
|
-
|
50
|
-
|
54
|
+
# Check if a request URL's path should be checked for a Wix signed instance or not. Checks the path against the paths option.
|
55
|
+
# @param [String] path URL path (=directory part) to check
|
56
|
+
# @return [Boolean] Indicates if given path should be checked or not
|
57
|
+
def path?(path)
|
58
|
+
path_match? paths, path
|
51
59
|
end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
'permissions' => @instance.permissions
|
61
|
+
# @param [Array<String,Regexp>] match_paths List of Strings (match exact path) amd Regexps (match whatever the regexp says)
|
62
|
+
# @param [String] path The path to match against
|
63
|
+
def path_match?(match_paths, path)
|
64
|
+
match_paths.any? { |match_path|
|
65
|
+
match_path === path
|
59
66
|
}
|
60
|
-
@request.GET['parsed_instance'] = parsed_instance
|
61
|
-
|
62
67
|
end
|
68
|
+
|
63
69
|
end
|
64
70
|
end
|
65
|
-
end
|
71
|
+
end
|