castle-rb 1.0.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +77 -0
  3. data/lib/castle.rb +47 -0
  4. data/lib/castle/client.rb +139 -0
  5. data/lib/castle/configuration.rb +37 -0
  6. data/lib/castle/errors.rb +16 -0
  7. data/lib/castle/ext/her.rb +14 -0
  8. data/lib/castle/jwt.rb +36 -0
  9. data/lib/castle/models/account.rb +11 -0
  10. data/lib/castle/models/backup_codes.rb +4 -0
  11. data/lib/castle/models/challenge.rb +7 -0
  12. data/lib/castle/models/context.rb +18 -0
  13. data/lib/castle/models/event.rb +7 -0
  14. data/lib/castle/models/model.rb +71 -0
  15. data/lib/castle/models/monitoring.rb +6 -0
  16. data/lib/castle/models/pairing.rb +11 -0
  17. data/lib/castle/models/recommendation.rb +3 -0
  18. data/lib/castle/models/session.rb +6 -0
  19. data/lib/castle/models/trusted_device.rb +5 -0
  20. data/lib/castle/models/user.rb +20 -0
  21. data/lib/castle/request.rb +164 -0
  22. data/lib/castle/session_store.rb +35 -0
  23. data/lib/castle/session_token.rb +39 -0
  24. data/lib/castle/support/cookie_store.rb +48 -0
  25. data/lib/castle/support/padrino.rb +19 -0
  26. data/lib/castle/support/rails.rb +11 -0
  27. data/lib/castle/support/sinatra.rb +17 -0
  28. data/lib/castle/token_store.rb +30 -0
  29. data/lib/castle/utils.rb +22 -0
  30. data/lib/castle/version.rb +3 -0
  31. data/spec/fixtures/vcr_cassettes/challenge_create.yml +48 -0
  32. data/spec/fixtures/vcr_cassettes/challenge_verify.yml +42 -0
  33. data/spec/fixtures/vcr_cassettes/session_create.yml +47 -0
  34. data/spec/fixtures/vcr_cassettes/session_refresh.yml +47 -0
  35. data/spec/fixtures/vcr_cassettes/session_verify.yml +47 -0
  36. data/spec/fixtures/vcr_cassettes/user_find.yml +44 -0
  37. data/spec/fixtures/vcr_cassettes/user_find_non_existing.yml +42 -0
  38. data/spec/fixtures/vcr_cassettes/user_import.yml +46 -0
  39. data/spec/fixtures/vcr_cassettes/user_update.yml +47 -0
  40. data/spec/helpers_spec.rb +38 -0
  41. data/spec/jwt_spec.rb +67 -0
  42. data/spec/models/challenge_spec.rb +18 -0
  43. data/spec/models/session_spec.rb +14 -0
  44. data/spec/models/user_spec.rb +31 -0
  45. data/spec/spec_helper.rb +29 -0
  46. data/spec/utils_spec.rb +59 -0
  47. metadata +273 -0
@@ -0,0 +1,6 @@
1
+ module Castle
2
+ class Monitoring < Model
3
+ collection_path '/v1' # Her doesn't accept the empty string
4
+ custom_post :heartbeat
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module Castle
2
+ class Pairing < Model
3
+ collection_path "users/:user_id/pairings"
4
+ instance_post :verify
5
+ instance_post :set_default!
6
+ belongs_to :user
7
+ has_one :config
8
+ end
9
+
10
+ class Config < Model; end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Castle
2
+ class Recommendation < Model; end
3
+ end
@@ -0,0 +1,6 @@
1
+ module Castle
2
+ class Session < Model
3
+ collection_path "users/:user_id/sessions"
4
+ has_one :context
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Castle
2
+ class TrustedDevice < Model
3
+ collection_path "users/:user_id/trusted_devices"
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module Castle
2
+ class User < Model
3
+ instance_post :enable_mfa!
4
+ instance_post :disable_mfa!
5
+
6
+ has_many :challenges
7
+ has_many :events
8
+ has_many :pairings
9
+ has_many :sessions
10
+ has_many :trusted_devices
11
+
12
+ def backup_codes(params={})
13
+ Castle::BackupCodes.get("/v1/users/#{id}/backup_codes", params)
14
+ end
15
+
16
+ def generate_backup_codes(params={})
17
+ Castle::BackupCodes.post("/v1/users/#{id}/backup_codes", params)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,164 @@
1
+ module Castle
2
+ module Request
3
+
4
+ def self.client_user_agent
5
+ @uname ||= get_uname
6
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
7
+
8
+ {
9
+ :bindings_version => Castle::VERSION,
10
+ :lang => 'ruby',
11
+ :lang_version => lang_version,
12
+ :platform => RUBY_PLATFORM,
13
+ :publisher => 'castle',
14
+ :uname => @uname
15
+ }
16
+ end
17
+
18
+ def self.get_uname
19
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
20
+ rescue Errno::ENOMEM # couldn't create subprocess
21
+ "uname lookup failed"
22
+ end
23
+
24
+ #
25
+ # Faraday middleware
26
+ #
27
+ module Middleware
28
+ # Sets credentials dynamically, allowing them to change between requests.
29
+ #
30
+ class BasicAuth < Faraday::Middleware
31
+ def initialize(app, api_secret)
32
+ super(app)
33
+ @api_secret = api_secret
34
+ end
35
+
36
+ def call(env)
37
+ value = Base64.encode64(":#{@api_secret || Castle.config.api_secret}")
38
+ value.delete!("\n")
39
+ env[:request_headers]["Authorization"] = "Basic #{value}"
40
+ @app.call(env)
41
+ end
42
+ end
43
+
44
+ # Handle request errors
45
+ #
46
+ class RequestErrorHandler < Faraday::Middleware
47
+ def call(env)
48
+ env.request.timeout = Castle.config.request_timeout
49
+ begin
50
+ @app.call(env)
51
+ rescue Faraday::ConnectionFailed
52
+ raise Castle::RequestError, 'Could not connect to Castle API'
53
+ rescue Faraday::TimeoutError
54
+ raise Castle::RequestError, 'Castle API timed out'
55
+ end
56
+ end
57
+ end
58
+
59
+ # Adds details about current environment
60
+ #
61
+ class EnvironmentHeaders < Faraday::Middleware
62
+ def call(env)
63
+ begin
64
+ env[:request_headers]["X-Castle-Client-User-Agent"] =
65
+ MultiJson.encode(Castle::Request.client_user_agent)
66
+ rescue # ignored
67
+ end
68
+
69
+ env[:request_headers]["User-Agent"] =
70
+ "Castle/v1 RubyBindings/#{Castle::VERSION}"
71
+
72
+ @app.call(env)
73
+ end
74
+ end
75
+
76
+ # Sends the active session token in a header, and extracts the returned
77
+ # session token and sets it locally.
78
+ #
79
+ class SessionToken < Faraday::Middleware
80
+ def call(env)
81
+ castle = RequestStore.store[:castle]
82
+ return @app.call(env) unless castle
83
+
84
+ # get the session token from our local store
85
+ if castle.session_token
86
+ env[:request_headers]['X-Castle-Session-Token'] =
87
+ castle.session_token.to_s
88
+ end
89
+
90
+ # call the API
91
+ response = @app.call(env)
92
+
93
+ # update the local store with the updated session token
94
+ token = response.env.response_headers['X-Castle-set-session-token']
95
+ castle.session_token = token if token
96
+
97
+ response
98
+ end
99
+ end
100
+
101
+ # Adds request context like IP address and user agent to any request.
102
+ #
103
+ class ContextHeaders < Faraday::Middleware
104
+ def call(env)
105
+ castle = RequestStore.store[:castle]
106
+ return @app.call(env) unless castle
107
+
108
+ castle.request_context.each do |key, value|
109
+ if value
110
+ header =
111
+ "X-Castle-#{key.to_s.gsub('_', '-').gsub(/\w+/) {|m| m.capitalize}}"
112
+ env[:request_headers][header] = value
113
+ end
114
+ end
115
+ @app.call(env)
116
+ end
117
+ end
118
+
119
+ class JSONParser < Faraday::Response::Middleware
120
+ def on_complete(env)
121
+ response = if env[:body].nil? || env[:body].empty?
122
+ {}
123
+ else
124
+ begin
125
+ MultiJson.load(env[:body], :symbolize_keys => true)
126
+ rescue MultiJson::LoadError
127
+ raise Castle::ApiError, 'Invalid response from Castle API'
128
+ end
129
+ end
130
+
131
+ case env[:status]
132
+ when 200..299
133
+ # OK
134
+ when 400
135
+ raise Castle::BadRequestError, response[:message]
136
+ when 401
137
+ raise Castle::UnauthorizedError, response[:message]
138
+ when 403
139
+ raise Castle::ForbiddenError, response[:message]
140
+ when 404
141
+ raise Castle::NotFoundError, response[:message]
142
+ when 419
143
+ # session token is invalid so clear it
144
+ RequestStore.store[:castle].session_token = nil
145
+
146
+ raise Castle::UserUnauthorizedError, response[:message]
147
+ when 422
148
+ raise Castle::InvalidParametersError, response[:message]
149
+ else
150
+ raise Castle::ApiError, response[:message]
151
+ end
152
+
153
+ env[:body] = {
154
+ data: response,
155
+ metadata: [],
156
+ errors: {}
157
+ }
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,35 @@
1
+ module Castle
2
+ class SessionStore
3
+ class Rack < SessionStore
4
+ def initialize(session)
5
+ @session = session
6
+ end
7
+
8
+ def user_id
9
+ @session['Castleuser_id']
10
+ end
11
+
12
+ def user_id=(value)
13
+ @session['Castleuser_id'] = value
14
+ end
15
+
16
+ def read
17
+ @session[key]
18
+ end
19
+
20
+ def write(value)
21
+ @session[key] = value
22
+ end
23
+
24
+ def destroy
25
+ @session.delete(key)
26
+ end
27
+
28
+ private
29
+
30
+ def key
31
+ "Castleuser.#{user_id}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ require 'jwt'
2
+
3
+ module Castle
4
+ class SessionToken
5
+ def initialize(token)
6
+ if token
7
+ @jwt = Castle::JWT.new(token)
8
+ end
9
+ end
10
+
11
+ def to_s
12
+ @jwt.to_token
13
+ end
14
+
15
+ def expired?
16
+ @jwt.expired?
17
+ end
18
+
19
+ def device_trusted?
20
+ @jwt.payload['tru'] == 1
21
+ end
22
+
23
+ def has_default_pairing?
24
+ @jwt.payload['dpr'] > 0
25
+ end
26
+
27
+ def mfa_enabled?
28
+ @jwt.payload['mfa'] == 1
29
+ end
30
+
31
+ def mfa_in_progress?
32
+ @jwt.payload['chg'] == 1
33
+ end
34
+
35
+ def mfa_required?
36
+ @jwt.payload['vfy'] > 0
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ module Castle
2
+ module CookieStore
3
+ class Base
4
+ def initialize(request, response)
5
+ @request = request
6
+ @response = response
7
+ end
8
+
9
+ def [](key)
10
+ @request.cookies[key]
11
+ end
12
+
13
+ def []=(key, value)
14
+ @request.cookies[key] = value
15
+ if value
16
+ @response.set_cookie(key, value: value,
17
+ expires: Time.now + (20 * 365 * 24 * 60 * 60),
18
+ path: '/')
19
+ else
20
+ @response.delete_cookie(key)
21
+ end
22
+ end
23
+ end
24
+
25
+ class Rack
26
+ def initialize(cookies)
27
+ @cookies = cookies
28
+ end
29
+
30
+ def [](key)
31
+ @cookies[key]
32
+ end
33
+
34
+ def []=(key, value)
35
+ if value
36
+ @cookies[key] = {
37
+ value: value,
38
+ expires: Time.now + (20 * 365 * 24 * 60 * 60),
39
+ path: '/'
40
+ }
41
+ else
42
+ @cookies.delete(key)
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'cookie_store'
2
+
3
+ module Padrino
4
+ class Application
5
+ module Castle
6
+ module Helpers
7
+ def castle
8
+ @castle ||= ::Castle::Client.new(request, response)
9
+ end
10
+ end
11
+
12
+ def self.registered(app)
13
+ app.helpers Helpers
14
+ end
15
+ end
16
+
17
+ register Castle
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Castle
2
+ module CastleClient
3
+ def castle
4
+ @castle ||= env['castle'] || Castle::Client.new(request, response)
5
+ end
6
+ end
7
+
8
+ ActiveSupport.on_load(:action_controller) do
9
+ include CastleClient
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'cookie_store'
2
+
3
+ module Sinatra
4
+ module Castle
5
+ module Helpers
6
+ def castle
7
+ @castle ||= ::Castle::Client.new(request, response)
8
+ end
9
+ end
10
+
11
+ def self.registered(app)
12
+ app.helpers Helpers
13
+ end
14
+ end
15
+
16
+ register Castle
17
+ end
@@ -0,0 +1,30 @@
1
+ module Castle
2
+ class TokenStore
3
+ def initialize(cookies)
4
+ @cookies = cookies
5
+ end
6
+
7
+ def session_token
8
+ token = @cookies['_ubs']
9
+ Castle::SessionToken.new(token) if token
10
+ end
11
+
12
+ def session_token=(value)
13
+ @cookies['_ubs'] = value
14
+
15
+ if value && value != @cookies['_ubs']
16
+ @cookies['_ubs']
17
+ elsif !value
18
+ @cookies['_ubs'] = nil
19
+ end
20
+ end
21
+
22
+ def trusted_device_token
23
+ @cookies['_ubt']
24
+ end
25
+
26
+ def trusted_device_token=(value)
27
+ @cookies['_ubt'] = value
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # TODO: scope Castle::Utils
2
+
3
+ module Castle
4
+ class << self
5
+ def setup_api(api_secret = nil)
6
+ api_endpoint = ENV.fetch('CASTLE_API_ENDPOINT') {
7
+ "https://api.castle.io/v1"
8
+ }
9
+
10
+ Her::API.setup url: api_endpoint do |c|
11
+ c.use Castle::Request::Middleware::BasicAuth, api_secret
12
+ c.use Castle::Request::Middleware::RequestErrorHandler
13
+ c.use Castle::Request::Middleware::EnvironmentHeaders
14
+ c.use Castle::Request::Middleware::ContextHeaders
15
+ c.use Castle::Request::Middleware::SessionToken
16
+ c.use FaradayMiddleware::EncodeJson
17
+ c.use Castle::Request::Middleware::JSONParser
18
+ c.use Faraday::Adapter::NetHttp
19
+ end
20
+ end
21
+ end
22
+ end