castle-rb 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/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
|