passageidentity 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c1466ff91a157560360908cbf7f037c11f14e7d4b5c7385ef8e8a41f8e33894
4
- data.tar.gz: 2edd43607d2994a65554d5e878a946ead5f1f26b0ad5b20b93351bd3dc4453a1
3
+ metadata.gz: 7f8ef6244146fba25dae09520b6c0b744ee2cf0be1b869f6d7e4a69611aa2376
4
+ data.tar.gz: eb737a9adbe33d1bf85788418083fed6a90231d2f9e35c0b5fa80131e910a54c
5
5
  SHA512:
6
- metadata.gz: 7ffcdb146558eb0df9664feaa13fe1f04388a7bad176330d863b588f881086567a0fbcf50c59c0f4831750284946d7cfd480d7bfe69f9e6ea3954bb640e2cd75
7
- data.tar.gz: 891d8a07de1f8522cce169a64bbaf211604600b67c61166ea358e3615b33a95baa447de68c271ae53bbd4c78ee0f9916f85635b8e7f69e2899cfd81b50673ad6
6
+ metadata.gz: 062fecbc902121ffe19c2d9b9422e9543d36982dce927782d85dfd5b0e91f616881bbd64294baf325ed4a74279658ef7a6f90d0974c7c05a734147d5aec2638a
7
+ data.tar.gz: 34ef84ee41bbe12b2a147ef01b6ee160d2676063cf49afe047ddc7473c583aba0bf97a862d07a4485fdd2bc63ad5bb97f86deb186d745e21aedf7431d563c446
data/.gitignore ADDED
@@ -0,0 +1,58 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ /tests/environment.rb
14
+
15
+ # Used by dotenv library to load environment variables.
16
+ # .env
17
+
18
+ # Ignore Byebug command history file.
19
+ .byebug_history
20
+
21
+ ## Specific to RubyMotion:
22
+ .dat*
23
+ .repl_history
24
+ build/
25
+ *.bridgesupport
26
+ build-iPhoneOS/
27
+ build-iPhoneSimulator/
28
+
29
+ ## Specific to RubyMotion (use of CocoaPods):
30
+ #
31
+ # We recommend against adding the Pods directory to your .gitignore. However
32
+ # you should judge for yourself, the pros and cons are mentioned at:
33
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
34
+ #
35
+ # vendor/Pods/
36
+
37
+ ## Documentation cache and generated files:
38
+ /.yardoc/
39
+ /_yardoc/
40
+ /doc/
41
+ /rdoc/
42
+
43
+ ## Environment normalization:
44
+ /.bundle/
45
+ /vendor/bundle
46
+ /lib/bundler/man/
47
+
48
+ # for a library or gem, you might want to ignore these files since the code is
49
+ # intended to run in multiple environments; otherwise, check them in:
50
+ # Gemfile.lock
51
+ # .ruby-version
52
+ # .ruby-gemset
53
+
54
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
55
+ .rvmrc
56
+
57
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
58
+ # .rubocop-https?--*
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,65 @@
1
+ # Ruby SDK for Passage
2
+
3
+ ## Testing the gem locally
4
+
5
+ Install the gem
6
+
7
+ ```
8
+ gem build passageidentity.gemspec
9
+ gem install ./passageidentity-0.0.1.gem
10
+ ```
11
+
12
+ Test it out:
13
+
14
+ ```
15
+ irb -Ilib -rpassageidentity
16
+ >> passage = Passage::Client.new(app_id: 'YOUR_APP_ID')
17
+ >> passage.auth.authenticate("JWT_HERE")
18
+ <passage_user_id>
19
+ ```
20
+
21
+ Run Tests:
22
+
23
+ ```
24
+ # all tests
25
+ ruby tests/all.rb
26
+ # individual test files
27
+ ruby tests/*_test.rb
28
+ ```
29
+
30
+ To test in the example app, change the Gemfile to include this path:
31
+
32
+ ```
33
+ gem "passageidentity", path: "../../passage-ruby"
34
+ ```
35
+
36
+ ## Publishing
37
+
38
+ Create an account in rubygems.org then run the following command with your username.
39
+
40
+ ```
41
+ $ curl -u <username> https://rubygems.org/api/v1/api_key.yaml >
42
+ ~/.gem/credentials; chmod 0600 ~/.gem/credentials
43
+
44
+ Enter host password for user '<username>':
45
+ ```
46
+
47
+ ```
48
+ <<<<<<< HEAD
49
+ gem push passage-0.0.0.gem
50
+ ```
51
+
52
+ You can check for the gem here:
53
+ ```
54
+ gem list -r passage
55
+ ```
56
+ =======
57
+ gem push passageidentity-0.0.0.gem
58
+ ```
59
+
60
+ You can check for the gem here:
61
+
62
+ ```
63
+ gem list -r passageidentity
64
+ ```
65
+ >>>>>>> 2d0e3f6dc3b40c621c8d16506fa6ab43b0fba673
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Passage
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ <img src="https://storage.googleapis.com/passage-docs/passage-logo-gradient.svg" alt="Passage logo" style="width:250px;"/>
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/passageidentity.svg)](https://badge.fury.io/rb/passageidentity)
4
+
5
+ # passage-ruby
6
+
7
+ This Ruby SDK allows for verification of server-side authentication and user management for Ruby applications build with [Passage](https://passage.id).
8
+
9
+ Install this package using [RubyGems](https://rubygems.org/gems/passageidentity).
10
+
11
+ ```
12
+ gem install passageidentity
13
+ ```
14
+
15
+ ## Instantiating the Passage Class
16
+
17
+ Initialize the Passage Client as follows:
18
+
19
+ ```ruby
20
+ PassageClient =
21
+ Passage::Client.new(
22
+ app_id: 'YOUR APP ID',
23
+ api_key: 'YOUR APIKEY',
24
+ auth_strategy: Passage::HEADER_AUTH,
25
+ )
26
+ ```
27
+
28
+ Passage has three arguments that can be used for initialization: `app_id`, `api_key`, and `auth_strategy`.
29
+
30
+ - `app_id` is the Passage App ID that specifies which app should be authorized. It has no default value and must to be set upon initialization.
31
+ - `api_key` is an API key for the Passage app, which can be generated in the 'App Settings' section of the [Passage Console](https://console.passage.id). It is an optional parameter and not required for authenticating requests. It is required to get or update user information.
32
+ - `auth_strategy` defines where the Passage SDK should look for the authentication token. It is set by default to `Passage::COOKIE_AUTH`, but can be changed to `Passage::HEADER_AUTH`.
33
+
34
+ ## Authenticating a Request
35
+
36
+ To authenticate an HTTP request in a Rails application, you can use the Passage library in a middleware function. You need to provide Passage with your App ID in order to verify the JWTs.
37
+
38
+ ```ruby
39
+ require 'passageidentity'
40
+
41
+ class ApplicationController < ActionController::Base
42
+ PassageClient = Passage::Client.new(app_id: PASSAGE_APP_ID)
43
+ def authorize!
44
+ begin
45
+ user_id = PassageClient.auth.authenticate_request(request)
46
+ # user is authorized
47
+ rescue Exception => e
48
+ # handle exception (user is not authorized)
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ ## Retrieve User Info
55
+
56
+ To retrieve information about a user, you should use the `get` method. You will need to use a Passage API key, which can be created in the Passage Console under your Application Settings. This API key grants your web server access to the Passage management APIs to get and update information about users. This API key must be protected and stored in an appropriate secure storage location. It should never be hard-coded in the repository.
57
+
58
+ ```ruby
59
+ require 'passageidentity'
60
+
61
+ class ApplicationController < ActionController::Base
62
+ PassageClient =
63
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
64
+ def authorize!
65
+ begin
66
+ user_id = PassageClient.auth.authenticate_request(request)
67
+ user = PassageClient.user.get(user_id: @user_id)
68
+ # user is authorized
69
+ rescue Exception => e
70
+ # handle exception (user is not authorized)
71
+ end
72
+ end
73
+ end
74
+ ```
75
+
76
+ The information available in the Passage User struct returned by PassageClient.user.get(user_id:):
77
+
78
+ ```ruby
79
+ Struct.new :id,
80
+ :status,
81
+ :email,
82
+ :phone,
83
+ :email_verified,
84
+ :created_at,
85
+ :updated_at,
86
+ :last_login_at,
87
+ :login_count,
88
+ :recent_events,
89
+ :webauthn,
90
+ :webauthn_devices,
91
+ :user_metadata,
92
+ ```
93
+
94
+ ## Activate/Deactivate User
95
+
96
+ You can activate or deactivate a user using the Passage SDK. These actions require an API Key and deactivating a user will prevent them from logging into your application
97
+ with Passage.
98
+
99
+ ```ruby
100
+ require 'passageidentity'
101
+
102
+ PassageClient =
103
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
104
+
105
+ user = PassageClient.user.deactivate(user_id: user_id)
106
+ user = PassageClient.user.activate(user_id: user_id)
107
+ ```
108
+
109
+ ## Create User
110
+
111
+ You can create users using their email address or phone number. Note that their phone number must be in E164 format (example shown below).
112
+
113
+ ```ruby
114
+ require 'passageidentity'
115
+
116
+ PassageClient =
117
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
118
+
119
+ user = PassageClient.user.create(email: 'exampleEmail@domain.com')
120
+ user = PassageClient.user.create(phone: '+15005550007')
121
+ ```
122
+
123
+ ## Delete User
124
+
125
+ You can delete a user using the Passage SDK.
126
+
127
+ ```ruby
128
+ require 'passageidentity'
129
+
130
+ PassageClient =
131
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
132
+
133
+ success = PassageClient.user.delete(user_id: user_id)
134
+ puts 'passage user was successfully deleted' if success
135
+ ```
136
+
137
+ ## List User Devices
138
+
139
+ You can list the devices associated with a particular Passage User.
140
+
141
+ ```ruby
142
+ require 'passageidentity'
143
+
144
+ PassageClient =
145
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
146
+
147
+ devices = PassageClient.user.list_devices(user_id: user_id)
148
+ ```
149
+
150
+ The information available in the array of Passage Device struct returned by PassageClient.user.list_devices(user_id:):
151
+
152
+ ```ruby
153
+ Struct.new :id,
154
+ :cred_id,
155
+ :friendly_name,
156
+ :usage_count,
157
+ :last_used,
158
+
159
+ ```
160
+
161
+ ## List User Devices
162
+
163
+ You can list the devices associated with a particular Passage User.
164
+
165
+ ```ruby
166
+ require 'passageidentity'
167
+
168
+ PassageClient = Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
169
+
170
+ success = PassageClient.user.delete_device(user_id: user_id, device_id)
171
+ if success
172
+ puts "passage user device was successfully deleted"
173
+ end
174
+ ```
175
+
176
+ ## Create an Embeddable Magic Link
177
+
178
+ To create a magic link, you should use the `create_magic_link` method. You can check out our guide on embeddable magic links in our [docs](https://docs.passage.id/popular-guides/smart-links).
179
+
180
+ ```ruby
181
+ require 'passageidentity'
182
+
183
+ PassageClient =
184
+ Passage::Client.new(app_id: PASSAGE_APP_ID, api_key: PASSAGE_API_KEY)
185
+
186
+ magic_link = PassageClient.create_magic_link(user_id: user_id)
187
+ magic_link =
188
+ PassageClient.create_magic_link(
189
+ email: 'example@domain.com',
190
+ send: true,
191
+ channel: Passage::EMAIL_CHANNEL,
192
+ ttl: 120,
193
+ )
194
+ ```
195
+
196
+ The information available in the Passage Magic Link struct returned this method is below:
197
+
198
+ ```ruby
199
+ Struct.new :id,
200
+ :secret,
201
+ :activated,
202
+ :user_id,
203
+ :app_id,
204
+ :identifier,
205
+ :type,
206
+ :redirect_url,
207
+ :ttl,
208
+ :url
209
+ ```
@@ -0,0 +1,35 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'jwt'
4
+
5
+ module Passage
6
+ class Auth
7
+
8
+ def initialize(app_id, public_key, auth_origin)
9
+
10
+ @app_id = app_id
11
+ @auth_origin = auth_origin
12
+
13
+ # bas64 decode and then parse the public key
14
+ # when we have JWKS endpoint, this will get easier I think
15
+ @public_key = OpenSSL::PKey::RSA.new(Base64.decode64(public_key))
16
+
17
+ end
18
+
19
+ def authenticate(token)
20
+
21
+ begin
22
+ claims = JWT.decode(token, @public_key, true,{ iss: @app_id, verify_iss: true, aud: @auth_origin, verify_aud: true, algorithms: ["RS256"] })
23
+ return claims[0]["sub"]
24
+ rescue JWT::InvalidIssuerError
25
+ raise JWTInvalidIssuerError
26
+ rescue JWT::InvalidAudError
27
+ raise JWTInvalidAudienceError
28
+ rescue JWT::ExpiredSignature
29
+ raise JWTExpiredSignatureError
30
+ rescue JWT::IncorrectAlgorithm
31
+ raise JWTIncorrectAlgorithmError
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'request'
4
+ require_relative 'auth'
5
+
6
+ module Passage
7
+ class Client
8
+
9
+ attr_reader :auth
10
+
11
+ def initialize(app_id:)
12
+ @api_url = "https://api.passage.id"
13
+ @app_id = app_id
14
+
15
+ get_connection()
16
+
17
+ fetch_public_key(@connection)
18
+
19
+ @auth = Passage::Auth.new(@app_id, @public_key, @auth_origin)
20
+ end
21
+
22
+ def get_connection
23
+ @connection = Faraday.new(url: @api_url) do |f|
24
+ f.request :json
25
+ f.request :retry
26
+ f.response :json
27
+ f.adapter :net_http
28
+ end
29
+ end
30
+
31
+ def fetch_public_key(conn)
32
+ response = conn.get("/v1/apps/" + @app_id)
33
+ # TODO Add error handling
34
+ @public_key = response.body["app"]["rsa_public_key"]
35
+ @auth_origin = response.body["app"]["auth_origin"]
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Passage
4
+ module Request
5
+ def get_request(path)
6
+ @connection.get(
7
+ path
8
+ ).body
9
+ end
10
+
11
+ def post_request(path, data)
12
+ @connection.post(
13
+ path,
14
+ data
15
+ ).body
16
+ end
17
+
18
+ def put_request(path, data)
19
+ @connection.put(
20
+ path,
21
+ data
22
+ ).body
23
+ end
24
+
25
+ def delete_request(path)
26
+ @connection.delete(
27
+ path
28
+ ).body
29
+ end
30
+
31
+ end
32
+ end
File without changes
data/lib/passage.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ require_relative 'passage/client'
6
+
7
+ module Passage end
8
+
9
+
@@ -0,0 +1,72 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'jwt'
4
+ require_relative 'client'
5
+
6
+ module Passage
7
+ class Auth
8
+ def initialize(app_id, auth_strategy, public_key, auth_origin)
9
+ @app_id = app_id
10
+ @auth_strategy = auth_strategy
11
+ @auth_origin = auth_origin
12
+
13
+ # bas64 decode and then parse the public key
14
+ # when we have JWKS endpoint, this will get easier I think
15
+ @public_key = OpenSSL::PKey::RSA.new(Base64.decode64(public_key))
16
+ end
17
+
18
+ def authenticate_request(request)
19
+ # Get the token based on the strategy
20
+ if @auth_strategy === Passage::COOKIE_STRATEGY
21
+ unless request.cookies['psg_auth_token'].present?
22
+ raise PassageError,
23
+ `missing authentication token: expected "psg_auth_token" cookie`
24
+ end
25
+ @token = request.cookies['psg_auth_token']
26
+ else
27
+ headers = request.headers
28
+ unless headers['Authorization'].present?
29
+ raise PassageError, 'no authentication token in header'
30
+ end
31
+ @token = headers['Authorization'].split(' ').last
32
+ end
33
+
34
+ # authenticate the token
35
+ if @token
36
+ return authenticate_token(@token)
37
+ else
38
+ raise PassageError, 'no authentication token'
39
+ end
40
+ nil
41
+ end
42
+
43
+ def authenticate_token(token)
44
+ begin
45
+ claims =
46
+ JWT.decode(
47
+ token,
48
+ @public_key,
49
+ true,
50
+ {
51
+ iss: @app_id,
52
+ verify_iss: true,
53
+ aud: @auth_origin,
54
+ verify_aud: true,
55
+ algorithms: ['RS256']
56
+ }
57
+ )
58
+ return claims[0]['sub']
59
+ rescue JWT::InvalidIssuerError => e
60
+ raise Passage::PassageError, e.message
61
+ rescue JWT::InvalidAudError => e
62
+ raise Passage::PassageError, e.message
63
+ rescue JWT::ExpiredSignature => e
64
+ raise Passage::PassageError, e.message
65
+ rescue JWT::IncorrectAlgorithm => e
66
+ raise Passage::PassageError, e.message
67
+ rescue JWT::DecodeError => e
68
+ raise Passage::PassageError, e.message
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auth'
4
+ require_relative 'user_api'
5
+ require_relative 'error'
6
+
7
+ module Passage
8
+ User =
9
+ Struct.new :id,
10
+ :status,
11
+ :email,
12
+ :phone,
13
+ :email_verified,
14
+ :created_at,
15
+ :updated_at,
16
+ :last_login_at,
17
+ :login_count,
18
+ :recent_events,
19
+ :webauthn,
20
+ :webauthn_devices,
21
+ :user_metadata,
22
+ keyword_init: true
23
+ MagicLink =
24
+ Struct.new :id,
25
+ :secret,
26
+ :activated,
27
+ :user_id,
28
+ :app_id,
29
+ :identifier,
30
+ :type,
31
+ :redirect_url,
32
+ :ttl,
33
+ :url,
34
+ keyword_init: true
35
+ Device =
36
+ Struct.new :id,
37
+ :cred_id,
38
+ :friendly_name,
39
+ :usage_count,
40
+ :last_used,
41
+ keyword_init: true
42
+
43
+ COOKIE_STRATEGY = 0
44
+ HEADER_STRATEGY = 1
45
+
46
+ EMAIL_CHANNEL = 'email'
47
+ PHONE_CHANNEL = 'phone'
48
+
49
+ class Client
50
+ @@app_cache = {}
51
+
52
+ attr_reader :auth
53
+ attr_reader :user
54
+
55
+ def initialize(app_id:, api_key: '', auth_strategy: COOKIE_STRATEGY)
56
+ @api_url = 'https://api.passage.id'
57
+ @app_id = app_id
58
+ @api_key = api_key
59
+
60
+ # check for valid auth strategy
61
+ unless [COOKIE_STRATEGY, HEADER_STRATEGY].include? auth_strategy
62
+ raise PassageError, 'invalid auth strategy.'
63
+ end
64
+ @auth_strategy = auth_strategy
65
+
66
+ # setup
67
+ get_connection
68
+ fetch_public_key(@connection)
69
+
70
+ # initialize auth class
71
+ @auth =
72
+ Passage::Auth.new(@app_id, @auth_strategy, @public_key, @auth_origin)
73
+
74
+ # initialize user class
75
+ @user = Passage::UserAPI.new(@connection, @app_id, @api_key)
76
+ end
77
+
78
+ def get_connection
79
+ if @api_key == ''
80
+ @connection =
81
+ Faraday.new(url: @api_url) do |f|
82
+ f.request :json
83
+ f.request :retry
84
+ f.response :raise_error
85
+ f.response :json
86
+ f.adapter :net_http
87
+ end
88
+ else
89
+ @connection =
90
+ Faraday.new(
91
+ url: @api_url,
92
+ headers: {
93
+ 'Authorization' => "Bearer #{@api_key}"
94
+ }
95
+ ) do |f|
96
+ f.request :json
97
+ f.request :retry
98
+ f.response :raise_error
99
+ f.response :json
100
+ f.adapter :net_http
101
+ end
102
+ end
103
+ end
104
+
105
+ def fetch_public_key(conn)
106
+ if @@app_cache[@app_id]
107
+ @public_key, @auth_origin = @@app_cache[@app_id]
108
+ else
109
+ # fetch the public key if not in cache
110
+ response = conn.get("/v1/apps/#{@app_id}")
111
+ @public_key = response.body['app']['rsa_public_key']
112
+ @auth_origin = response.body['app']['auth_origin']
113
+ @@app_cache[@app_id] ||= [@public_key, @auth_origin]
114
+ end
115
+ end
116
+
117
+ def create_magic_link(
118
+ user_id: '',
119
+ email: '',
120
+ phone: '',
121
+ channel: '',
122
+ send: false,
123
+ magic_link_path: '',
124
+ redirect_url: '',
125
+ ttl: 60
126
+ )
127
+ magic_link_req = {}
128
+ magic_link_req['user_id'] = user_id unless user_id.empty?
129
+ magic_link_req['email'] = email unless email.empty?
130
+ magic_link_req['phone'] = phone unless phone.empty?
131
+
132
+ # check to see if the channel specified is valid before sending it off to the server
133
+ unless [PHONE_CHANNEL, EMAIL_CHANNEL].include? channel
134
+ raise PassageError,
135
+ 'channel: must be either Passage::EMAIL_CHANNEL or Passage::PHONE_CHANNEL'
136
+ end
137
+ magic_link_req['channel'] = channel unless channel.empty?
138
+ magic_link_req['send'] = send
139
+ magic_link_req['magic_link_path'] = magic_link_path unless magic_link_path
140
+ .empty?
141
+ magic_link_req['redirect_url'] = redirect_url unless redirect_url.empty?
142
+ magic_link_req['ttl'] = ttl unless ttl == 0
143
+
144
+ begin
145
+ response =
146
+ @connection.post("/v1/apps/#{@app_id}/magic-links", magic_link_req)
147
+ magic_link = response.body['magic_link']
148
+ return(
149
+ Passage::MagicLink.new(
150
+ id: magic_link['id'],
151
+ secret: magic_link['secret'],
152
+ activated: magic_link['activated'],
153
+ user_id: magic_link['user_id'],
154
+ app_id: magic_link['app_id'],
155
+ identifier: magic_link['identifier'],
156
+ type: magic_link['type'],
157
+ redirect_url: magic_link['redirect_url'],
158
+ ttl: magic_link['ttl'],
159
+ url: magic_link['url']
160
+ )
161
+ )
162
+ rescue Faraday::Error => e
163
+ raise PassageError,
164
+ "failed to create Passage Magic Link. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,4 @@
1
+ module Passage
2
+ class PassageError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,241 @@
1
+ require_relative 'client'
2
+
3
+ module Passage
4
+ class UserAPI
5
+ # This class will require an API key
6
+ def initialize(connection, app_id, api_key)
7
+ @connection = connection
8
+ @app_id = app_id
9
+ @api_key = api_key
10
+ end
11
+
12
+ def get(user_id:)
13
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
14
+ begin
15
+ response = @connection.get("/v1/apps/#{@app_id}/users/#{user_id}")
16
+ user = response.body['user']
17
+ user.transform_keys(&:to_sym)
18
+ return(
19
+ Passage::User.new(
20
+ id: user['id'],
21
+ status: user['status'],
22
+ email: user['email'],
23
+ phone: user['phone'],
24
+ email_verified: user['email_verified'],
25
+ created_at: user['created_at'],
26
+ updated_at: user['updated_at'],
27
+ last_login_at: user['last_login_at'],
28
+ login_count: user['login_count'],
29
+ webauthn: user['webauthn'],
30
+ webauthn_devices: user['webbauthn_devices'],
31
+ recent_events: user['recent_events'],
32
+ user_metadata: user['user_metadata']
33
+ )
34
+ )
35
+ rescue Faraday::Error => e
36
+ if e.is_a? Faraday::ResourceNotFound
37
+ raise PassageError,
38
+ "passage User with ID \"#{user_id}\" does not exist"
39
+ else
40
+ raise PassageError,
41
+ "failed to get Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
42
+ end
43
+ end
44
+ end
45
+
46
+ def activate(user_id:)
47
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
48
+ begin
49
+ response =
50
+ @connection.patch("/v1/apps/#{@app_id}/users/#{user_id}/activate")
51
+ user = response.body['user']
52
+ return(
53
+ Passage::User.new(
54
+ id: user['id'],
55
+ status: user['status'],
56
+ email: user['email'],
57
+ phone: user['phone'],
58
+ email_verified: user['email_verified'],
59
+ created_at: user['created_at'],
60
+ updated_at: user['updated_at'],
61
+ last_login_at: user['last_login_at'],
62
+ login_count: user['login_count'],
63
+ webauthn: user['webauthn'],
64
+ webauthn_devices: user['webbauthn_devices'],
65
+ recent_events: user['recent_events'],
66
+ user_metadata: user['user_metadata']
67
+ )
68
+ )
69
+ rescue Faraday::Error => e
70
+ if e.is_a? Faraday::ResourceNotFound
71
+ raise PassageError,
72
+ "passage User with ID \"#{user_id}\" does not exist"
73
+ else
74
+ raise PassageError,
75
+ "failed to activate Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
76
+ end
77
+ end
78
+ end
79
+
80
+ def deactivate(user_id:)
81
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
82
+ begin
83
+ response =
84
+ @connection.patch("/v1/apps/#{@app_id}/users/#{user_id}/deactivate")
85
+ user = response.body['user']
86
+ return(
87
+ Passage::User.new(
88
+ id: user['id'],
89
+ status: user['status'],
90
+ email: user['email'],
91
+ phone: user['phone'],
92
+ email_verified: user['email_verified'],
93
+ created_at: user['created_at'],
94
+ updated_at: user['updated_at'],
95
+ last_login_at: user['last_login_at'],
96
+ login_count: user['login_count'],
97
+ webauthn: user['webauthn'],
98
+ webauthn_devices: user['webbauthn_devices'],
99
+ recent_events: user['recent_events'],
100
+ user_metadata: user['user_metadata']
101
+ )
102
+ )
103
+ rescue Faraday::Error => e
104
+ if e.is_a? Faraday::ResourceNotFound
105
+ raise PassageError,
106
+ "passage User with ID \"#{user_id}\" does not exist"
107
+ else
108
+ raise PassageError,
109
+ "failed to deactivate Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
110
+ end
111
+ end
112
+ end
113
+
114
+ def update(user_id:, email: '', phone: '', user_metadata: {})
115
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
116
+ updates = {}
117
+ updates['email'] = email unless email.empty?
118
+ updates['phone'] = phone unless phone.empty?
119
+ updates['user_metadata'] = user_metadata unless user_metadata.empty?
120
+ begin
121
+ response =
122
+ @connection.patch("/v1/apps/#{@app_id}/users/#{user_id}", updates)
123
+ user = response.body['user']
124
+ return(
125
+ Passage::User.new(
126
+ id: user['id'],
127
+ status: user['status'],
128
+ email: user['email'],
129
+ phone: user['phone'],
130
+ email_verified: user['email_verified'],
131
+ created_at: user['created_at'],
132
+ updated_at: user['updated_at'],
133
+ last_login_at: user['last_login_at'],
134
+ login_count: user['login_count'],
135
+ webauthn: user['webauthn'],
136
+ webauthn_devices: user['webbauthn_devices'],
137
+ recent_events: user['recent_events'],
138
+ user_metadata: user['user_metadata']
139
+ )
140
+ )
141
+ rescue Faraday::Error => e
142
+ if e.is_a? Faraday::ResourceNotFound
143
+ raise PassageError,
144
+ "passage User with ID \"#{user_id}\" does not exist"
145
+ else
146
+ raise PassageError,
147
+ "failed to update Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
148
+ end
149
+ end
150
+ end
151
+
152
+ def create(email: '', phone: '', user_metadata: {})
153
+ create = {}
154
+ create['email'] = email unless email.empty?
155
+ create['phone'] = phone unless phone.empty?
156
+ create['user_metadata'] = user_metadata unless user_metadata.empty?
157
+ begin
158
+ response = @connection.post("/v1/apps/#{@app_id}/users", create)
159
+ user = response.body['user']
160
+ return(
161
+ Passage::User.new(
162
+ id: user['id'],
163
+ status: user['status'],
164
+ email: user['email'],
165
+ phone: user['phone'],
166
+ email_verified: user['email_verified'],
167
+ created_at: user['created_at'],
168
+ updated_at: user['updated_at'],
169
+ last_login_at: user['last_login_at'],
170
+ login_count: user['login_count'],
171
+ webauthn: user['webauthn'],
172
+ webauthn_devices: user['webbauthn_devices'],
173
+ recent_events: user['recent_events'],
174
+ user_metadata: user['user_metadata']
175
+ )
176
+ )
177
+ rescue Faraday::Error => e
178
+ raise PassageError,
179
+ "failed to create Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
180
+ end
181
+ end
182
+
183
+ def delete(user_id:)
184
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
185
+ begin
186
+ response = @connection.delete("/v1/apps/#{@app_id}/users/#{user_id}")
187
+ return true
188
+ rescue Faraday::Error => e
189
+ if e.is_a? Faraday::ResourceNotFound
190
+ raise PassageError,
191
+ "passage User with ID \"#{user_id}\" does not exist"
192
+ else
193
+ raise PassageError,
194
+ "failed to delete Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
195
+ end
196
+ end
197
+ end
198
+
199
+ def delete_device(user_id:, device_id:)
200
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
201
+ if device_id.to_s.empty?
202
+ raise PassageError, 'must supply a valid device_id'
203
+ end
204
+ begin
205
+ response =
206
+ @connection.delete(
207
+ "/v1/apps/#{@app_id}/users/#{user_id}/devices/#{device_id}"
208
+ )
209
+ return true
210
+ rescue Faraday::Error => e
211
+ raise PassageError,
212
+ "failed to delete Passage User Device. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
213
+ end
214
+ end
215
+
216
+ def list_devices(user_id:)
217
+ raise PassageError, 'must supply a valid user_id' if user_id.to_s.empty?
218
+ begin
219
+ response =
220
+ @connection.get("/v1/apps/#{@app_id}/users/#{user_id}/devices")
221
+ devicesResp = response.body['devices']
222
+ devices = Array.new
223
+ devicesResp.each do |device|
224
+ devices.append(
225
+ Passage::Device.new(
226
+ id: device['id'],
227
+ cred_id: device['cred_id'],
228
+ friendly_name: device['friendly_name'],
229
+ usage_count: device['usage_count'],
230
+ last_used: device['last_used']
231
+ )
232
+ )
233
+ end
234
+ return devices
235
+ rescue Faraday::Error => e
236
+ raise PassageError,
237
+ "failed to delete Passage User Device. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
238
+ end
239
+ end
240
+ end
241
+ end
@@ -4,6 +4,5 @@ require 'faraday'
4
4
 
5
5
  require_relative 'passageidentity/client'
6
6
 
7
- module Passage end
8
-
9
-
7
+ module Passage
8
+ end
data/passage-ruby ADDED
@@ -0,0 +1 @@
1
+ passage-ruby
data/passage.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'passage'
3
+ s.version = '0.0.1'
4
+ s.summary = "Passage SDK for biometric authentication"
5
+ s.description = "Enables verification of server-side authentication and user management for applications using Passage"
6
+ s.authors = ["Passage Identity"]
7
+ s.email = 'support@passage.id'
8
+ s.files = ["lib/passage.rb"]
9
+ s.homepage =
10
+ 'https://rubygems.org/gems/passage'
11
+ s.license = 'MIT'
12
+
13
+ s.metadata['source_code_uri'] = 'https://github.com/passage-identity/passage-ruby'
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_dependency 'faraday', '>= 0.17.0', '< 2.0'
20
+ s.add_dependency 'jwt', '>= 2.3.0'
21
+ s.add_dependency 'openssl', '>= 3.0.0'
22
+ end
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'passageidentity'
3
+ s.version = '0.0.2'
4
+ s.summary = 'Passage SDK for biometric authentication'
5
+ s.description =
6
+ 'Enables verification of server-side authentication and user management for applications using Passage'
7
+ s.authors = ['Passage Identity']
8
+ s.email = 'support@passage.id'
9
+ s.files = ['lib/passageidentity.rb']
10
+ s.homepage = 'https://rubygems.org/gems/passageidentity'
11
+ s.license = 'MIT'
12
+
13
+ s.metadata['source_code_uri'] =
14
+ 'https://github.com/passage-identity/passage-ruby'
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ s.require_paths = ['lib']
19
+
20
+ s.files =
21
+ Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ end
26
+
27
+ s.add_dependency 'faraday', '>= 0.17.0', '< 2.0'
28
+ s.add_dependency 'jwt', '>= 2.3.0'
29
+ s.add_dependency 'openssl', '>= 3.0.0'
30
+ end
data/tests/all.rb ADDED
@@ -0,0 +1,2 @@
1
+ Dir[File.dirname(File.absolute_path(__FILE__)) + '/**/*_test.rb']
2
+ .each { |file| require file }
@@ -0,0 +1,20 @@
1
+ require_relative '../lib/passageidentity/client'
2
+ require_relative './environment'
3
+ require 'faraday'
4
+ require 'test/unit'
5
+
6
+ class TestUserAPI < Test::Unit::TestCase
7
+ PassageClient =
8
+ Passage::Client.new(app_id: ENV['APP_ID'], api_key: ENV['API_KEY'])
9
+
10
+ def test_create_magi_link()
11
+ magic_link =
12
+ PassageClient.create_magic_link(
13
+ email: 'chris@passage.id',
14
+ channel: Passage::EMAIL_CHANNEL,
15
+ ttl: 12
16
+ )
17
+ assert_equal 12, magic_link.ttl
18
+ assert_equal 'chris@passage.id', magic_link.identifier
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ require_relative '../lib/passageidentity/client'
2
+ require_relative './environment'
3
+ require 'faraday'
4
+ require 'test/unit'
5
+
6
+ class TestUserAPI < Test::Unit::TestCase
7
+ PassageClient =
8
+ Passage::Client.new(app_id: ENV['APP_ID'], api_key: ENV['API_KEY'])
9
+
10
+ def setup()
11
+ $global_test_user =
12
+ PassageClient.user.create(
13
+ email: 'chris+test-ruby@passage.id',
14
+ user_metadata: {
15
+ 'example1': 'cool'
16
+ }
17
+ )
18
+ end
19
+
20
+ def test_create_delete_user()
21
+ user =
22
+ PassageClient.user.create(
23
+ email: 'chris+test-create-delete@passage.id',
24
+ user_metadata: {
25
+ 'example1': 'cool'
26
+ }
27
+ )
28
+ assert_equal 'chris+test-create-delete@passage.id', user.email
29
+ assert_equal 'cool', user.user_metadata['example1']
30
+ deleted = PassageClient.user.delete(user_id: user.id)
31
+ assert_equal true, deleted
32
+ end
33
+
34
+ def test_get_user()
35
+ user = PassageClient.user.get(user_id: $global_test_user.id)
36
+ assert_equal $global_test_user.id, user.id
37
+ end
38
+
39
+ def test_deactivate_user()
40
+ user = PassageClient.user.deactivate(user_id: $global_test_user.id)
41
+ assert_equal $global_test_user.id, user.id
42
+ assert_equal 'inactive', user.status
43
+ end
44
+
45
+ def test_activate_user()
46
+ user = PassageClient.user.activate(user_id: $global_test_user.id)
47
+ assert_equal $global_test_user.id, user.id
48
+ assert_equal 'active', user.status
49
+ end
50
+
51
+ def test_update_user()
52
+ new_email = 'chris+update_test-ruby@passage.id'
53
+ user =
54
+ PassageClient.user.update(
55
+ user_id: $global_test_user.id,
56
+ email: new_email,
57
+ user_metadata: {
58
+ 'example1': 'lame'
59
+ }
60
+ )
61
+ assert_equal $global_test_user.id, user.id
62
+ assert_equal new_email, user.email
63
+ assert_equal 'lame', user.user_metadata['example1']
64
+ end
65
+
66
+ def test_list_devices()
67
+ devices = PassageClient.user.list_devices(user_id: $global_test_user.id)
68
+ assert_equal [], devices
69
+ end
70
+
71
+ def teardown()
72
+ deleted = PassageClient.user.delete(user_id: $global_test_user.id)
73
+ end
74
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passageidentity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Passage Identity
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-18 00:00:00.000000000 Z
11
+ date: 2022-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -65,7 +65,26 @@ executables: []
65
65
  extensions: []
66
66
  extra_rdoc_files: []
67
67
  files:
68
+ - ".gitignore"
69
+ - CONTRIBUTING.md
70
+ - LICENSE
71
+ - README.md
72
+ - lib/passage.rb
73
+ - lib/passage/auth.rb
74
+ - lib/passage/client.rb
75
+ - lib/passage/request.rb
76
+ - lib/passage/user.rb
68
77
  - lib/passageidentity.rb
78
+ - lib/passageidentity/auth.rb
79
+ - lib/passageidentity/client.rb
80
+ - lib/passageidentity/error.rb
81
+ - lib/passageidentity/user_api.rb
82
+ - passage-ruby
83
+ - passage.gemspec
84
+ - passageidentity.gemspec
85
+ - tests/all.rb
86
+ - tests/magic_link_test.rb
87
+ - tests/user_api_test.rb
69
88
  homepage: https://rubygems.org/gems/passageidentity
70
89
  licenses:
71
90
  - MIT