faria-launchpad-api 0.2.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: e8a9269575d3372e1fccc706b7929f81cd848d5d
4
+ data.tar.gz: 7d54bd4d87bd350cad342443299df497d334fa7f
5
+ SHA512:
6
+ metadata.gz: 70e2c202b5f22143824ac304bb535a20170939878df370f863bfa26d0eea38520cb6b73ec261edaa4cce09b49f35f477147acd26ee647565d0daaec21b6eef4f
7
+ data.tar.gz: 335ed431202e3731bdf92cfd52153b155a4e7a37ce794100df2eb35887563ef80bfe24c5c0f66d3ac3411b52be18d2d118d5703fcce13048c94de0c9f250e3fa
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in faria-launchpad-api.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Josh Goebel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # faria-launchpad-api
2
+
3
+ This gem will help you to integrate your Ruby (or Ruby on Rails) application with Faria's [LaunchPad](https://dev.faria.co/launchpad/) platform.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'faria-launchpad-api'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install faria-launchpad-api
21
+
22
+
23
+ ## Usage
24
+
25
+ ### Generating a RSA public/private keypair
26
+
27
+ #### With the OpenSSL library inside Ruby
28
+
29
+ ```ruby
30
+ # first setup a local keypair
31
+ key = OpenSSL::PKey::RSA.generate(2048)
32
+ File.open("./secure/private_key", "w") do |f|
33
+ f.write(key.to_s)
34
+ end
35
+ # this is the public key LaunchPad needs
36
+ puts key.public_key.to_s
37
+ ```
38
+
39
+ #### With the console OpenSSL tools
40
+
41
+ ```bash
42
+ % openssl genrsa -out private_key.pem 2048
43
+ Generating RSA private key, 2048 bit long modulus
44
+ ..+++
45
+ ............................................+++
46
+ e is 65537 (0x10001)
47
+
48
+ % openssl rsa -pubout -in private_key.pem
49
+ writing RSA key
50
+ -----BEGIN PUBLIC KEY-----
51
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6GUAjWeb1uyHJXhwBLtt
52
+ 402PRlzHmMzK66b0Y+LKM789JaMO/8lOrCuoTtYkiWUpOU+7Qu6fBAMAGhCLYnOP
53
+ nMAftBbGN2Ppd64QYiAUTh/8pYtR36q88E7H74ngEHN/cBN8JXD4yqPo219/IyZs
54
+ uPIhJZgZ4DRGFanoilTYBOj8mH0hWVnFuwLrT6Qc0ibrIqyrQ4QP2NiM1CZlEO7t
55
+ lqJUm/bPgZdBqnQjbnfAmeyNRdsyeBhQvYhMLujdLpKQChYL64hAuj9X7ey8gZx5
56
+ rEaPECzlieoKcd3GL5KL+9g0vfvp8ZRyl54BgyDdS2P0p3r6xWqk/CTWjN+aAv/c
57
+ kQIDAQAB
58
+ -----END PUBLIC KEY-----
59
+ ```
60
+
61
+
62
+ #### Serving your public key inside your application
63
+
64
+ The LaunchPad spec also requires that your web application serve this public key (text/plaintext) over HTTPs as part of the API your application must furnish to fully support LaunchPad.
65
+
66
+ GET https://yourapplication.com/apis/launchpad/pubkey
67
+ -----BEGIN PUBLIC KEY-----
68
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6GUAjWeb1uyHJXhwBLtt
69
+ 402PRlzHmMzK66b0Y+LKM789JaMO/8lOrCuoTtYkiWUpOU+7Qu6fBAMAGhCLYnOP
70
+ nMAftBbGN2Ppd64QYiAUTh/8pYtR36q88E7H74ngEHN/cBN8JXD4yqPo219/IyZs
71
+ uPIhJZgZ4DRGFanoilTYBOj8mH0hWVnFuwLrT6Qc0ibrIqyrQ4QP2NiM1CZlEO7t
72
+ lqJUm/bPgZdBqnQjbnfAmeyNRdsyeBhQvYhMLujdLpKQChYL64hAuj9X7ey8gZx5
73
+ rEaPECzlieoKcd3GL5KL+9g0vfvp8ZRyl54BgyDdS2P0p3r6xWqk/CTWjN+aAv/c
74
+ -----END PUBLIC KEY-----
75
+
76
+ ### Quick Usage Example
77
+
78
+ Here is a quick example. This example presumes you've already generated a 2,048 bit RSA keypair and your public key has been successfully connected with LaunchPad.
79
+
80
+
81
+ ```ruby
82
+ # fetch the LaunchPad public key
83
+ # (you should probably save it locally rather than constantly fetch it)
84
+ uri = "https://devel.launchpad.managebac.com/api/v1/"
85
+ launchpad_key = Net::HTTP.get_response(URI.parse(url + "pubkey")).body
86
+ local_key = OpenSSL::PKey::RSA.new(File.read("./secure/private_key"))
87
+
88
+ @service = Faria::Launchpad::Service.new(
89
+ "https://devel.launchpad.managebac.com/api/v1/",
90
+ {
91
+ keys: { local: local_key, remote: launchpad_key },
92
+ # the application name and URI issued to you during your
93
+ # LaunchPad setup process
94
+ source: {
95
+ name: "Acme Widgets, LLC.",
96
+ uri: "https://app.acmewidgets.com/"
97
+ }
98
+ }
99
+ )
100
+
101
+ # simple ping
102
+ puts service.ping()
103
+ # debugging info
104
+ puts service.info()
105
+ # echos back whatever you send
106
+ puts service.echo(name: "George Washington", age: 32)
107
+ ```
108
+
109
+ The responses returned will almost always be JSON responses (see [API documentation](https://dev.faria.co/launchpad/) for additional details.)
110
+
111
+ ### Rails Integration
112
+
113
+ There is a module to extend controllers to support easily handling incoming JWE requests and a Rails helper to assist with POSTing signed redirects. Below is a usage example.
114
+
115
+ If the URL includes query parameters they will be stripped from the URL and encoded into the JWE as signed parameters.
116
+
117
+ The `SSO` module below is just one example of how you might wrap up all the pieces of a LaunchPad SSO configuration. You might want to name this differently or pull settings from YAML, ENV, etc. For the helpers to work you must call `launchpad_config` from your controller class and pass it an object that responds to `keys` and `source` and returns those settings.
118
+
119
+ ```ruby
120
+ module SSO
121
+ def self.client
122
+ Faria::Launchpad::Service.new(launchpad_uri,
123
+ source: source,
124
+ keys: keys
125
+ )
126
+ end
127
+
128
+ def self.launchpad_uri
129
+ "http://launchpad.dev/api/v1/"
130
+ end
131
+
132
+ def self.source
133
+ @source ||= {
134
+ name: "Lurn2Spell",
135
+ uri: "http://lurn2spell.dev/launchpad/api/"
136
+ }
137
+ end
138
+
139
+ def self.keys
140
+ @keys ||= {
141
+ local: OpenSSL::PKey::RSA.new(File.read('config/keys/local.priv')),
142
+ remote: OpenSSL::PKey::RSA.new(File.read('config/keys/launchpad.pub'))
143
+ }
144
+ end
145
+ end
146
+
147
+ class YourController < ActionController::Base
148
+ include Faria::Launchpad::Controller
149
+ launchpad_config SSO
150
+
151
+ def action
152
+ post_encrypted_redirect_to SSO.client.pairing_request_url,
153
+ params_to_pass
154
+ end
155
+
156
+ end
157
+ ```
158
+
159
+
160
+ ## Development
161
+
162
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
163
+
164
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
165
+
166
+
167
+ ## Contributing
168
+
169
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eduvo/launchpad_api.
170
+
171
+
172
+ ## License
173
+
174
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
175
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "faria/launchpad/api"
5
+
6
+ require 'jwt'
7
+ require 'jwe'
8
+
9
+ # You can add fixtures and/or initialization code here to make experimenting
10
+ # with your gem easier. You can also use a different console, if you like.
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ # require "pry"
14
+ # Pry.start
15
+
16
+ require "irb"
17
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'faria/launchpad/api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "faria-launchpad-api"
8
+ spec.version = Faria::Launchpad::Api::VERSION
9
+ spec.authors = ["Josh Goebel"]
10
+ spec.email = ["me@joshgoebel.com"]
11
+
12
+ spec.summary = %q{Ruby library to interface with Faria LaunchPad.}
13
+ spec.description = %q{Ruby library to interface with Faria LaunchPad, including an API client and Rails helpers.}
14
+ spec.homepage = "https://github.com/eduvo/launchpad-api"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ # additional files to not package in the gem
27
+ %w(.gitignore .travis.yml bin/test.rb).each do |file|
28
+ spec.files.reject! {|f| f == file}
29
+ end
30
+ # puts spec.files
31
+ spec.bindir = "bin"
32
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", "~> 1.11"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "minitest", "~> 5.0"
38
+
39
+ # need 1.5.4 since they have been changing the component API a lot in minor
40
+ # point releases
41
+ spec.add_dependency "jwt", "~> 1.5.4"
42
+ spec.add_dependency "jwe", "~> 0.1.0"
43
+ spec.add_dependency "addressable", "~> 2.4"
44
+ end
@@ -0,0 +1,7 @@
1
+ module Faria
2
+ module Launchpad
3
+ module Api
4
+ VERSION = "0.2.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require "faria/launchpad/api/version"
2
+ require_relative "./packet"
3
+ require_relative "./service"
4
+ require_relative "./helper"
5
+ require_relative "./controller" if defined?(ActiveSupport)
6
+
7
+ module Faria
8
+ module Launchpad
9
+ module API
10
+ # Your code goes here...
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,76 @@
1
+ module Faria
2
+ module Launchpad
3
+
4
+ # module to include in Rails controllers to make sending/receiving
5
+ # of JWE signed/encrypted packets much simpler
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def launchpad_config(config=nil)
11
+ return @launchpad_config if config.nil?
12
+
13
+ @launchpad_config = config
14
+ end
15
+ end
16
+
17
+ included do
18
+ helper Faria::Launchpad::Helper
19
+ end
20
+
21
+ private
22
+
23
+ def handle_jwe_payload(request)
24
+ if request.content_type == "application/jwe"
25
+ packet = request.body.read()
26
+ elsif params[:payload].present? && params[:content_type] == "application/jwe"
27
+ packet = params[:payload]
28
+ else
29
+ packet = request.headers["Faria-JWE"]
30
+ end
31
+ return unless packet.present?
32
+ logger.info "packet is #{packet}"
33
+
34
+ keys = self.class.launchpad_config.keys
35
+ data = Faria::Launchpad::Packet.decrypt(
36
+ packet,
37
+ { actual_url: request.original_url },
38
+ local_key: keys[:local],
39
+ remote_key: keys[:remote]
40
+ )
41
+
42
+ if request.post? || request.put?
43
+ Rails.logger.info " Parameters (JWT): #{data.inspect}"
44
+ data.with_indifferent_access
45
+ else
46
+ params.except(:controller, :action)
47
+ end
48
+ end
49
+
50
+ def post_encrypted_redirect_to(url, params = {})
51
+ config = self.class.launchpad_config
52
+
53
+ uri = Addressable::URI.parse(url)
54
+ params = (uri.query_values || {}).merge(params)
55
+ uri.query_values = nil
56
+
57
+ payload = Faria::Launchpad::Packet.encrypt(
58
+ params,
59
+ {
60
+ api_url: uri.normalize.to_s,
61
+ source: config.source,
62
+ expires_in: 60
63
+ },
64
+ local_key: config.keys[:local],
65
+ remote_key: config.keys[:remote]
66
+ )
67
+
68
+ render html: view_context.post_encrypted_redirect_to(
69
+ uri.normalize.to_s,
70
+ payload
71
+ )
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,21 @@
1
+ module Faria
2
+ module Launchpad
3
+
4
+ # Rails helper for generating encrypted POST redirect forms
5
+ module Helper
6
+
7
+ def post_encrypted_redirect_to(url, payload)
8
+ [
9
+ "<noscript>Your browser has Javascript disabled, please enable it.</noscript>".html_safe,
10
+ form_tag(url, authenticity_token: false, id: "frm"),
11
+ hidden_field_tag("content_type", "application/jwe"),
12
+ hidden_field_tag("payload", payload),
13
+ # submit_tag("submit"),
14
+ "</form>".html_safe,
15
+ javascript_tag("document.getElementById('frm').submit();")
16
+ ].join("\n").html_safe
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,125 @@
1
+ require 'jwe'
2
+ require 'jwt'
3
+ require 'json'
4
+ require 'addressable'
5
+
6
+ module Faria
7
+ module Launchpad
8
+ module Packet
9
+
10
+ VERSION = "v0.2"
11
+
12
+ class MismatchedRequestURL < StandardError
13
+ end
14
+
15
+ class MissingRemoteKey < StandardError
16
+ end
17
+
18
+ class ExpiredSignature < StandardError
19
+ attr_accessor :expired_by
20
+ def initialize(msg, expired_by)
21
+ super(msg)
22
+ @expired_by = expired_by
23
+ end
24
+ end
25
+
26
+ # encrypting is done with Launchpad public key
27
+ # signing is done with local private key
28
+
29
+ def self.encrypt(data, options = {}, local_key:, remote_key: )
30
+ packet = { "data" => data}
31
+ packet = add_issued_at(packet)
32
+ packet = add_expires(packet, options[:expires_in]) if options[:expires_in]
33
+ packet = add_api_url(packet, options[:api_url]) if options[:api_url]
34
+ # packet = add_issuer(packet, options[:issuer])
35
+ packet = add_source(packet, options[:source]) if options[:source]
36
+
37
+ payload = JWT.encode(packet, local_key, 'RS512')
38
+ "#{VERSION};" + JWE.encrypt(payload, remote_key) # public
39
+ end
40
+
41
+ # for cases where you known in advance the remote key to use (such
42
+ # as Launchpad clients which will only be receiving messages from
43
+ # Launchpad and therefore will only use it's public key for verifying
44
+ # signatures
45
+ def self.decrypt(raw_data, options = {}, local_key:, remote_key: )
46
+ version, jwe = raw_data.split(";", 2)
47
+ jwt = JWE.decrypt(jwe, local_key)
48
+ arr = JWT.decode(jwt, remote_key, true, { :algorithm => 'RS512' })
49
+ payload, header = arr
50
+
51
+ # validate_expiration will be handled by JWT decode
52
+ validate_url!(payload, options[:actual_url])
53
+
54
+ payload["data"]
55
+ end
56
+
57
+ # for cases where the signature key is not known in advance and must
58
+ # be determined by source information embedded in the JWT header
59
+ def self.decrypt_variable_key(raw_data, options = {}, local_key:, remote_key_func: )
60
+ version, jwe = raw_data.split(";", 2)
61
+ jwt = JWE.decrypt(jwe, local_key)
62
+ header, payload = JWT::Decode.new(jwt, nil, false, {}).decode_segments[0..1]
63
+ remote_key = remote_key_func.call(header, payload)
64
+ fail(MissingRemoteKey) if remote_key.nil?
65
+
66
+ arr = JWT.decode(jwt, remote_key, true, { :algorithm => 'RS512' })
67
+ payload, header = arr
68
+
69
+ # validate_expiration will be handled by JWT decode
70
+ validate_url!(payload, options[:actual_url])
71
+
72
+ payload["data"]
73
+ end
74
+
75
+ private
76
+
77
+ def self.add_source(packet, source)
78
+ packet[:faria_source] = source
79
+ packet
80
+ end
81
+
82
+ def self.add_issuer(packet, issuer)
83
+ packet[:iss] = issuer
84
+ packet
85
+ end
86
+
87
+ def self.add_api_url(packet, url)
88
+ packet["api_url"] = Addressable::URI.parse(url).normalize.to_s
89
+ packet
90
+ end
91
+
92
+ def self.add_issued_at(packet)
93
+ packet[:iat] = Time.now.utc.to_i
94
+ packet
95
+ end
96
+
97
+ def self.add_expires(packet, expires_in)
98
+ packet[:exp] = Time.now.utc.to_i + expires_in
99
+ packet
100
+ end
101
+
102
+ def self.validate_url!(payload, actual_url)
103
+ return unless payload.include?('api_url')
104
+
105
+ normalized_url = Addressable::URI.parse(actual_url).normalize.to_s
106
+ if payload['api_url'] != normalized_url
107
+ fail(MismatchedRequestURL)
108
+ end
109
+ end
110
+
111
+ def self.validate_expiration!(payload)
112
+ return unless payload.include?('exp')
113
+ leeway = 0
114
+
115
+ valid_until = (Time.now.utc.to_i - leeway)
116
+ if payload['exp'].to_i < valid_until
117
+ diff = valid_until - payload['exp'].to_i
118
+ error = ExpiredSignature.new("Signature expired", diff)
119
+ fail(error)
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,212 @@
1
+ require 'net/https'
2
+ require 'addressable'
3
+
4
+ module Faria
5
+ module Launchpad
6
+ class Service
7
+
8
+ LAUNCHPAD_NAME = "Launchpad"
9
+
10
+ def self.noauth(endpoint, quiet: false)
11
+ unless quiet
12
+ puts "************************************************************************\n" \
13
+ "\007\007\007NOTICE: noauth is only intended as a somewhat easy way to call `ping`\n" \
14
+ "and `pubkey`. Nothing else is going to work since keys are required for\n" \
15
+ "general API usage.\n" \
16
+ "************************************************************************\n"
17
+ sleep 2
18
+ end
19
+ new(endpoint, keys: { local: nil, remote: nil }, source: {name: "No one"})
20
+ end
21
+
22
+ DEFAULTS = {
23
+ expires_in: 60 # 1 minute
24
+ }
25
+
26
+ def initialize(endpoint, options = {})
27
+ @endpoint = endpoint
28
+ @my_key = options[:keys][:local]
29
+ @remote_key = options[:keys][:remote]
30
+ @source = options[:source]
31
+ @app_name = options[:source][:name]
32
+
33
+ @options = DEFAULTS.merge(options)
34
+ end
35
+
36
+ # utils
37
+
38
+ def ping
39
+ get_without_auth "ping"
40
+ end
41
+
42
+ def pubkey
43
+ resp = raw_get_without_auth("pubkey")
44
+ return resp.body if resp.code == '200'
45
+ end
46
+
47
+ # utils requiring auth
48
+
49
+ def info
50
+ get "info"
51
+ end
52
+
53
+ def echo(params={})
54
+ put "echo", params
55
+ end
56
+
57
+ # sessions
58
+
59
+ def retrieve_session(session_id, params = {})
60
+ get "authentication_sessions/#{session_id}", params
61
+ end
62
+
63
+ # data is intended to be JSON encoded data if passed
64
+ def approve_session(session_id, data = {})
65
+ params = data.empty? ? {} : { data: data }
66
+ post "authentication_sessions/#{session_id}/approve", params
67
+ end
68
+
69
+ # data is intended to be JSON encoded data if passed
70
+ def decline_session(session_id, data = {})
71
+ params = data.empty? ? {} : { data: data }
72
+ post "authentication_sessions/#{session_id}/decline", params
73
+ end
74
+
75
+ # identities
76
+
77
+ def show_identity(uuid)
78
+ get "identities/#{uuid}"
79
+ end
80
+
81
+ def update_identity(identity_representation, uuid)
82
+ patch "identities/#{uuid}", identity: identity_representation
83
+ end
84
+
85
+ # by_value allows the unique pairing value to be used to perform
86
+ # queries or updates instead of Launchpad's internal UUID
87
+ def show_identity_by_pairing_value(pairing_value)
88
+ get "identities/by_pairing_value/#{pairing_value}"
89
+ end
90
+
91
+ def update_identity_by_pairing_value(identity_representation, pairing_value)
92
+ patch "identities/by_pairing_value/#{pairing_value}", identity: identity_representation
93
+ end
94
+
95
+ # final provisioning step (server side)
96
+ def provision(params = {})
97
+ raise "you need an :approval_code" if params[:approval_code].blank?
98
+ raise "you need an :identity" if params[:identity].blank?
99
+
100
+ post("pairing/provision", params)
101
+ end
102
+
103
+ # direct methods (for undocumented api?)
104
+
105
+ def post(url, params = {})
106
+ resp = raw_request(:post, url, params)
107
+ parse_response(resp)
108
+ end
109
+
110
+ def get(url, params = {})
111
+ resp = raw_request(:get, url, params)
112
+ parse_response(resp)
113
+ end
114
+
115
+ def put(url, params = {})
116
+ resp = raw_request(:put, url, params)
117
+ parse_response(resp)
118
+ end
119
+
120
+ def patch(url, params = {})
121
+ resp = raw_request(:patch, url, params)
122
+ parse_response(resp)
123
+ end
124
+
125
+ def parse_response(resp)
126
+ hash = JSON.parse(resp.body)
127
+ # be railsy if we can
128
+ hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
129
+ hash
130
+ rescue JSON::ParserError
131
+ raise JSON::ParserError, resp.body
132
+ end
133
+
134
+ # lower-level HTTP code
135
+
136
+ def get_without_auth(url, params={})
137
+ parse_response raw_get_without_auth(url, params)
138
+ end
139
+
140
+ def raw_get_without_auth(url, params={})
141
+ uri = full_url(url)
142
+ Net::HTTP.get_response(URI(uri))
143
+ end
144
+
145
+ def raw_request(verb, url, params = {})
146
+ uri = full_url(url)
147
+ a = Addressable::URI.parse(uri)
148
+ Net::HTTP.start(a.host, a.inferred_port) do |http|
149
+ http.use_ssl = a.scheme == 'https'
150
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
151
+ request = verb_to_http_class(verb).new a.request_uri
152
+ payload = encrypt_payload(params, a)
153
+ if verb == :get
154
+ request['Faria-JWE'] = payload
155
+ else
156
+ request['Content-Type'] = "application/jwe"
157
+ request.body = payload
158
+ end
159
+ http.request request
160
+ end
161
+ end
162
+
163
+ # url helpers
164
+
165
+ def pairing_request_url
166
+ rooted_url "third/pairing/request"
167
+ end
168
+
169
+ def pairing_complete_url
170
+ rooted_url "third/pairing/complete"
171
+ end
172
+
173
+ private
174
+
175
+ VALID_VERBS = %w(get put patch post get delete)
176
+
177
+ # can't guarantee we have Rails or AS so we use eval vs
178
+ # constantize/classify, etc
179
+ def verb_to_http_class(verb)
180
+ raise "#{verb} is not a valid HTTP verb." unless VALID_VERBS.include?(verb.to_s)
181
+
182
+ Net::HTTP.const_get(verb.to_s.capitalize)
183
+ end
184
+
185
+ def encrypt_payload(params, address)
186
+ Faria::Launchpad::Packet.encrypt(
187
+ params,
188
+ {
189
+ api_url: address.normalize.to_s,
190
+ source: @source,
191
+ expires_in: @options[:expires_in]
192
+ },
193
+ remote_key: @remote_key,
194
+ local_key: @my_key
195
+ )
196
+ end
197
+
198
+ def rooted_url(url)
199
+ File.join(base_url(@endpoint), url)
200
+ end
201
+
202
+ def base_url(url)
203
+ url.gsub(%r{/api/v[^/]+/$},"")
204
+ end
205
+
206
+ def full_url(url)
207
+ File.join(@endpoint, url)
208
+ end
209
+
210
+ end
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faria-launchpad-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Goebel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: jwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.5.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.5.4
69
+ - !ruby/object:Gem::Dependency
70
+ name: jwe
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: addressable
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.4'
97
+ description: Ruby library to interface with Faria LaunchPad, including an API client
98
+ and Rails helpers.
99
+ email:
100
+ - me@joshgoebel.com
101
+ executables:
102
+ - console
103
+ - setup
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - faria-launchpad-api.gemspec
114
+ - lib/faria/launchpad/api.rb
115
+ - lib/faria/launchpad/api/version.rb
116
+ - lib/faria/launchpad/controller.rb
117
+ - lib/faria/launchpad/helper.rb
118
+ - lib/faria/launchpad/packet.rb
119
+ - lib/faria/launchpad/service.rb
120
+ homepage: https://github.com/eduvo/launchpad-api
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ allowed_push_host: https://rubygems.org
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.4.5
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Ruby library to interface with Faria LaunchPad.
145
+ test_files: []