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.
- checksums.yaml +7 -0
- data/README.md +77 -0
- data/lib/castle.rb +47 -0
- data/lib/castle/client.rb +139 -0
- data/lib/castle/configuration.rb +37 -0
- data/lib/castle/errors.rb +16 -0
- data/lib/castle/ext/her.rb +14 -0
- data/lib/castle/jwt.rb +36 -0
- data/lib/castle/models/account.rb +11 -0
- data/lib/castle/models/backup_codes.rb +4 -0
- data/lib/castle/models/challenge.rb +7 -0
- data/lib/castle/models/context.rb +18 -0
- data/lib/castle/models/event.rb +7 -0
- data/lib/castle/models/model.rb +71 -0
- data/lib/castle/models/monitoring.rb +6 -0
- data/lib/castle/models/pairing.rb +11 -0
- data/lib/castle/models/recommendation.rb +3 -0
- data/lib/castle/models/session.rb +6 -0
- data/lib/castle/models/trusted_device.rb +5 -0
- data/lib/castle/models/user.rb +20 -0
- data/lib/castle/request.rb +164 -0
- data/lib/castle/session_store.rb +35 -0
- data/lib/castle/session_token.rb +39 -0
- data/lib/castle/support/cookie_store.rb +48 -0
- data/lib/castle/support/padrino.rb +19 -0
- data/lib/castle/support/rails.rb +11 -0
- data/lib/castle/support/sinatra.rb +17 -0
- data/lib/castle/token_store.rb +30 -0
- data/lib/castle/utils.rb +22 -0
- data/lib/castle/version.rb +3 -0
- data/spec/fixtures/vcr_cassettes/challenge_create.yml +48 -0
- data/spec/fixtures/vcr_cassettes/challenge_verify.yml +42 -0
- data/spec/fixtures/vcr_cassettes/session_create.yml +47 -0
- data/spec/fixtures/vcr_cassettes/session_refresh.yml +47 -0
- data/spec/fixtures/vcr_cassettes/session_verify.yml +47 -0
- data/spec/fixtures/vcr_cassettes/user_find.yml +44 -0
- data/spec/fixtures/vcr_cassettes/user_find_non_existing.yml +42 -0
- data/spec/fixtures/vcr_cassettes/user_import.yml +46 -0
- data/spec/fixtures/vcr_cassettes/user_update.yml +47 -0
- data/spec/helpers_spec.rb +38 -0
- data/spec/jwt_spec.rb +67 -0
- data/spec/models/challenge_spec.rb +18 -0
- data/spec/models/session_spec.rb +14 -0
- data/spec/models/user_spec.rb +31 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/utils_spec.rb +59 -0
- metadata +273 -0
@@ -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,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
|
data/lib/castle/utils.rb
ADDED
@@ -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
|