pusher-platform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d95d9cc5e0f7c22f8ca7408c9486fb2b464c0572
4
+ data.tar.gz: 6ea7d2d88d3242661f2a8d1bf4531034a979abaf
5
+ SHA512:
6
+ metadata.gz: 78c889277b27b346069674ec5555ed3169fde818a39a3c925600eeeb704171e79d47a767bf5662dec3fdb867c220f17e9d5239232f7f71e557ef26ad5ce65227
7
+ data.tar.gz: 53e074a29c83777a6d47dd346db179ecc68076eeca6da81a1e3cd0854a3dd639cc11f92104cfa0197d0f758fefaaab7927a2c0b6ef4611732808ff1a4f58baec
@@ -0,0 +1 @@
1
+ require 'pusher-platform/app'
@@ -0,0 +1,69 @@
1
+ require_relative './authenticator'
2
+ require_relative './base_client'
3
+ require_relative './common'
4
+ require_relative './error_response'
5
+
6
+ module Pusher
7
+ class App
8
+ def initialize(options)
9
+ raise "Invalid app ID" if options[:app_id].nil?
10
+ @app_id = options[:app_id]
11
+
12
+ app_key_parts = /^([^:]+):(.+)$/.match(options[:app_key])
13
+ raise "Invalid app key" if app_key_parts.nil?
14
+
15
+ @app_key_id = app_key_parts[1]
16
+ @app_key_secret = app_key_parts[2]
17
+
18
+ @client = if options[:client]
19
+ options[:client]
20
+ else
21
+ raise "Invalid cluster" if options[:cluster].nil?
22
+ BaseClient.new(host: options[:cluster])
23
+ end
24
+
25
+ @authenticator = Authenticator.new(@app_id, @app_key_id, @app_key_secret)
26
+ end
27
+
28
+ def request(options)
29
+ options = scope_request_options("apps", options)
30
+ if options[:jwt].nil?
31
+ options = options.merge({ jwt: generate_superuser_jwt() })
32
+ end
33
+ @client.request(options)
34
+ end
35
+
36
+ def config_request(options)
37
+ options = scope_request_options("config/apps", options)
38
+ if options[:jwt].nil?
39
+ options = options.merge({ jwt: generate_superuser_jwt() })
40
+ end
41
+ @client.request(options)
42
+ end
43
+
44
+ def authenticate(request, options)
45
+ @authenticator.authenticate(request, options)
46
+ end
47
+
48
+ private
49
+
50
+ def scope_request_options(prefix, options)
51
+ path = "/#{prefix}/#{@app_id}/#{options[:path]}"
52
+ .gsub(/\/+/, "/")
53
+ .gsub(/\/+$/, "")
54
+ options.merge({ path: path })
55
+ end
56
+
57
+ def generate_superuser_jwt
58
+ now = Time.now.utc.to_i
59
+ claims = {
60
+ app: @app_id,
61
+ iss: "keys/#{@app_key_id}",
62
+ su: true,
63
+ iat: now - 30, # some leeway for the server
64
+ exp: now + 60*5, # 5 minutes should be enough for a single request
65
+ }
66
+ JWT.encode(claims, @app_key_secret)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,136 @@
1
+ require 'jwt'
2
+ require 'rack'
3
+
4
+ module Pusher
5
+ TOKEN_LEEWAY = 30
6
+ TOKEN_EXPIRY = 24*60*60
7
+
8
+ class Authenticator
9
+ def initialize(app_id, app_key_id, app_key_secret)
10
+ @app_id = app_id
11
+ @app_key_id = app_key_id
12
+ @app_key_secret = app_id
13
+ end
14
+
15
+ # Takes a Rack request to the authorization endpoint and and handles it
16
+ # either returning a new access/refresh token pair, or an error.
17
+ #
18
+ # @param request [Rack::Request] the request to authenticate
19
+ # @return the response object
20
+ def authenticate(request, options)
21
+ form_data = Rack::Utils.parse_nested_query request.body.read
22
+ grant_type = form_data['grant_type']
23
+
24
+ if grant_type == "client_credentials"
25
+ return authenticate_with_client_credentials(options)
26
+ elsif grant_type == "refresh_token"
27
+ old_refresh_jwt = form_data['refresh_token']
28
+ return authenticate_with_refresh_token(old_refresh_jwt, options)
29
+ else
30
+ return response(401, {
31
+ error: "unsupported_grant_type"
32
+ })
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def authenticate_with_client_credentials(options)
39
+ return respond_with_new_token_pair(options)
40
+ end
41
+
42
+ def authenticate_with_refresh_token(old_refresh_jwt, options)
43
+ old_refresh_token = begin
44
+ JWT.decode(old_refresh_jwt, @app_key_secret, true, {
45
+ iss: "keys/#{@app_key_id}",
46
+ verify_iss: true,
47
+ leeway: 30,
48
+ }).first
49
+ rescue => e
50
+ error_description = if e.is_a?(JWT::InvalidIssuerError)
51
+ "refresh token issuer is invalid"
52
+ elsif e.is_a?(JWT::ImmatureSignature)
53
+ "refresh token is not valid yet"
54
+ elsif e.is_a?(JWT::ExpiredSignature)
55
+ "refresh tokan has expired"
56
+ else
57
+ "refresh token is invalid"
58
+ end
59
+
60
+ return response(401, {
61
+ error: "invalid_grant",
62
+ error_description: error_description,
63
+ # TODO error_uri
64
+ })
65
+ end
66
+
67
+ if old_refresh_token["refresh"] != true
68
+ return response(401, {
69
+ error: "invalid_grant",
70
+ error_description: "refresh token does not have a refresh claim",
71
+ # TODO error_uri
72
+ })
73
+ end
74
+
75
+ if options[:user_id] != old_refresh_token["sub"]
76
+ return response(401, {
77
+ error: "invalid_grant",
78
+ error_description: "refresh token has an invalid user id",
79
+ # TODO error_uri
80
+ })
81
+ end
82
+
83
+ return respond_with_new_token_pair(options)
84
+ end
85
+
86
+ # Creates a payload dictionary made out of access and refresh token pair and TTL for the access token.
87
+ #
88
+ # @param user_id [String] optional id of the user, ignore for anonymous users
89
+ # @return [Hash] Payload as a hash
90
+ def respond_with_new_token_pair(options)
91
+ access_token = generate_access_token(options)
92
+ refresh_token = generate_refresh_token(options)
93
+ return response(200, {
94
+ access_token: access_token,
95
+ token_type: "bearer",
96
+ expires_in: TOKEN_EXPIRY,
97
+ refresh_token: refresh_token,
98
+ })
99
+ end
100
+
101
+ def generate_access_token(options)
102
+ now = Time.now.utc.to_i
103
+
104
+ claims = {
105
+ app: @app_id,
106
+ iss: "keys/#{@app_key_id}",
107
+ iat: now - TOKEN_LEEWAY,
108
+ exp: now + TOKEN_EXPIRY + TOKEN_LEEWAY,
109
+ sub: options[:user_id],
110
+ }
111
+
112
+ JWT.encode(claims, @app_key_secret, "HS256")
113
+ end
114
+
115
+ def generate_refresh_token(options)
116
+ now = Time.now.utc.to_i
117
+
118
+ claims = {
119
+ app: @app_id,
120
+ iss: "keys/#{@app_key_id}",
121
+ iat: now - TOKEN_LEEWAY,
122
+ refresh: true,
123
+ sub: options[:user_id],
124
+ }
125
+
126
+ JWT.encode(claims, @app_key_secret, "HS256")
127
+ end
128
+
129
+ def response(status, body)
130
+ return {
131
+ status: status,
132
+ json: body,
133
+ }
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,48 @@
1
+ require 'excon'
2
+ require 'json'
3
+
4
+ module Pusher
5
+ class BaseClient
6
+ def initialize(options)
7
+ raise "Unspecified host" if options[:host].nil?
8
+ @connection = Excon.new("https://#{options[:host]}")
9
+ end
10
+
11
+ def request(options)
12
+ raise "Unspecified request method" if options[:method].nil?
13
+ raise "Unspecified request path" if options[:path].nil?
14
+
15
+ headers = if options[:headers]
16
+ options[:headers].dup
17
+ else
18
+ {}
19
+ end
20
+
21
+ if options[:jwt]
22
+ headers["Authorization"] = "Bearer #{options[:jwt]}"
23
+ end
24
+
25
+ response = @connection.request(
26
+ method: options[:method],
27
+ path: options[:path],
28
+ headers: headers,
29
+ body: options[:body],
30
+ )
31
+
32
+ if response.status >= 200 && response.status <= 299
33
+ return response
34
+ elsif response.status >= 300 && response.status <= 399
35
+ raise "unsupported redirect response: #{response.status}"
36
+ elsif response.status >= 400 && response.status <= 599
37
+ error_body = begin
38
+ JSON.parse(response.body)
39
+ rescue
40
+ response.body
41
+ end
42
+ raise ErrorResponse.new(response.status, response.headers, error_body)
43
+ else
44
+ raise "unsupported response code: #{response.status}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,4 @@
1
+ module Pusher
2
+ class Error < ::StandardError
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module Pusher
2
+ class ErrorResponse < Error
3
+ attr_accessor :status, :headers, :body
4
+
5
+ def initialize(status, headers, body)
6
+ @status = status
7
+ @headers = headers
8
+ @body = body
9
+ end
10
+
11
+ def to_s
12
+ "Pusher::ErrorResponse: #{status} #{body}"
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pusher-platform
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pusher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: excon
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.54.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.54.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 1.5.6
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1.5'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.5.6
47
+ - !ruby/object:Gem::Dependency
48
+ name: rack
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ description:
62
+ email: support@pusher.com
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - lib/pusher-platform.rb
68
+ - lib/pusher-platform/app.rb
69
+ - lib/pusher-platform/authenticator.rb
70
+ - lib/pusher-platform/base_client.rb
71
+ - lib/pusher-platform/common.rb
72
+ - lib/pusher-platform/error_response.rb
73
+ homepage:
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.5.2
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Pusher Platform Ruby SDK
97
+ test_files: []