pusher-platform 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []