passageidentity 0.0.1 → 0.0.4
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 +4 -4
- data/.gitignore +58 -0
- data/CONTRIBUTING.md +71 -0
- data/LICENSE +21 -0
- data/README.md +209 -0
- data/lib/passageidentity/auth.rb +115 -0
- data/lib/passageidentity/client.rb +152 -0
- data/lib/passageidentity/error.rb +4 -0
- data/lib/passageidentity/user_api.rb +241 -0
- data/lib/passageidentity.rb +2 -3
- data/passage-ruby +1 -0
- data/passageidentity.gemspec +30 -0
- data/tests/all.rb +2 -0
- data/tests/auth_test.rb +14 -0
- data/tests/magic_link_test.rb +20 -0
- data/tests/user_api_test.rb +74 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5dd45f9023ac4cd3eb005f4604f40229981aeca5c6da2ebd1eca685fcf54245
|
4
|
+
data.tar.gz: de14152791bbe75fb11de58d65cf72bab61f570bec22f93240f7a88c128d8eaa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7184b5a3325f54c022a0ba63c622ad38670793eeb9c3cabadb0c28c4ee9b9675f63d302a93449f1d5eac43cecbbdf0bb82c0bbb1d06132caf3d3a99f552d3d1
|
7
|
+
data.tar.gz: 65e9721e3ca3049a758eb0287f5f379b7e27ee43397cb75592c2875194155a9d83d95c47244716d9de09d88fb6bb9ae33115d4f45649d6095adfb15c5b562942
|
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,71 @@
|
|
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
|
+
```
|
55
|
+
gem list -r passage
|
56
|
+
```
|
57
|
+
|
58
|
+
=======
|
59
|
+
gem push passageidentity-0.0.0.gem
|
60
|
+
|
61
|
+
```
|
62
|
+
|
63
|
+
You can check for the gem here:
|
64
|
+
|
65
|
+
```
|
66
|
+
|
67
|
+
gem list -r passageidentity
|
68
|
+
|
69
|
+
```
|
70
|
+
>>>>>>> 2d0e3f6dc3b40c621c8d16506fa6ab43b0fba673
|
71
|
+
```
|
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
|
+
[](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,115 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
require 'jwt'
|
4
|
+
require_relative 'client'
|
5
|
+
|
6
|
+
module Passage
|
7
|
+
class Auth
|
8
|
+
@@app_cache = {}
|
9
|
+
def initialize(app_id, auth_strategy, connection)
|
10
|
+
@app_id = app_id
|
11
|
+
@auth_strategy = auth_strategy
|
12
|
+
@connection = connection
|
13
|
+
|
14
|
+
fetch_jwks
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch_app()
|
18
|
+
begin
|
19
|
+
response = @connection.get("/v1/apps/#{@app_id}")
|
20
|
+
return response.body['app']
|
21
|
+
rescue Faraday::Error => e
|
22
|
+
raise PassageError,
|
23
|
+
"failed to get Passage User. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_jwks()
|
28
|
+
if @@app_cache[@app_id]
|
29
|
+
@jwks, @auth_origin = @@app_cache[@app_id]
|
30
|
+
else
|
31
|
+
app = fetch_app
|
32
|
+
auth_gw_connection =
|
33
|
+
Faraday.new(url: 'https://auth.passage.id') do |f|
|
34
|
+
f.request :json
|
35
|
+
f.request :retry
|
36
|
+
f.response :raise_error
|
37
|
+
f.response :json
|
38
|
+
f.adapter :net_http
|
39
|
+
end
|
40
|
+
|
41
|
+
# fetch the public key if not in cache
|
42
|
+
app = fetch_app
|
43
|
+
@auth_origin = app['auth_origin']
|
44
|
+
response =
|
45
|
+
auth_gw_connection.get("/v1/apps/#{@app_id}/.well-known/jwks.json")
|
46
|
+
@jwks = response.body
|
47
|
+
@@app_cache[@app_id] ||= [@jwks, @auth_origin]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def authenticate_request(request)
|
52
|
+
# Get the token based on the strategy
|
53
|
+
if @auth_strategy === Passage::COOKIE_STRATEGY
|
54
|
+
unless request.cookies['psg_auth_token'].present?
|
55
|
+
raise PassageError,
|
56
|
+
`missing authentication token: expected "psg_auth_token" cookie`
|
57
|
+
end
|
58
|
+
@token = request.cookies['psg_auth_token']
|
59
|
+
else
|
60
|
+
headers = request.headers
|
61
|
+
unless headers['Authorization'].present?
|
62
|
+
raise PassageError, 'no authentication token in header'
|
63
|
+
end
|
64
|
+
@token = headers['Authorization'].split(' ').last
|
65
|
+
end
|
66
|
+
|
67
|
+
# authenticate the token
|
68
|
+
if @token
|
69
|
+
return authenticate_token(@token)
|
70
|
+
else
|
71
|
+
raise PassageError, 'no authentication token'
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def authenticate_token(token)
|
77
|
+
kid = JWT.decode(token, nil, false)[1]['kid']
|
78
|
+
exists = false
|
79
|
+
for jwk in @jwks['keys']
|
80
|
+
if jwk['kid'] == kid
|
81
|
+
exists = true
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
fetch_jwks unless exists
|
86
|
+
begin
|
87
|
+
claims =
|
88
|
+
JWT.decode(
|
89
|
+
token,
|
90
|
+
nil,
|
91
|
+
true,
|
92
|
+
{
|
93
|
+
iss: @app_id,
|
94
|
+
verify_iss: true,
|
95
|
+
aud: @auth_origin,
|
96
|
+
verify_aud: true,
|
97
|
+
algorithms: ['RS256'],
|
98
|
+
jwks: @jwks
|
99
|
+
}
|
100
|
+
)
|
101
|
+
return claims[0]['sub']
|
102
|
+
rescue JWT::InvalidIssuerError => e
|
103
|
+
raise Passage::PassageError, e.message
|
104
|
+
rescue JWT::InvalidAudError => e
|
105
|
+
raise Passage::PassageError, e.message
|
106
|
+
rescue JWT::ExpiredSignature => e
|
107
|
+
raise Passage::PassageError, e.message
|
108
|
+
rescue JWT::IncorrectAlgorithm => e
|
109
|
+
raise Passage::PassageError, e.message
|
110
|
+
rescue JWT::DecodeError => e
|
111
|
+
raise Passage::PassageError, e.message
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,152 @@
|
|
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
|
+
attr_reader :auth
|
51
|
+
attr_reader :user
|
52
|
+
|
53
|
+
def initialize(app_id:, api_key: '', auth_strategy: COOKIE_STRATEGY)
|
54
|
+
@api_url = 'https://api.passage.id'
|
55
|
+
@app_id = app_id
|
56
|
+
@api_key = api_key
|
57
|
+
|
58
|
+
# check for valid auth strategy
|
59
|
+
unless [COOKIE_STRATEGY, HEADER_STRATEGY].include? auth_strategy
|
60
|
+
raise PassageError, 'invalid auth strategy.'
|
61
|
+
end
|
62
|
+
@auth_strategy = auth_strategy
|
63
|
+
|
64
|
+
# setup
|
65
|
+
get_connection
|
66
|
+
|
67
|
+
# initialize auth class
|
68
|
+
@auth = Passage::Auth.new(@app_id, @auth_strategy, @connection)
|
69
|
+
|
70
|
+
# initialize user class
|
71
|
+
@user = Passage::UserAPI.new(@connection, @app_id, @api_key)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_connection
|
75
|
+
if @api_key == ''
|
76
|
+
@connection =
|
77
|
+
Faraday.new(url: @api_url) do |f|
|
78
|
+
f.request :json
|
79
|
+
f.request :retry
|
80
|
+
f.response :raise_error
|
81
|
+
f.response :json
|
82
|
+
f.adapter :net_http
|
83
|
+
end
|
84
|
+
else
|
85
|
+
@connection =
|
86
|
+
Faraday.new(
|
87
|
+
url: @api_url,
|
88
|
+
headers: {
|
89
|
+
'Authorization' => "Bearer #{@api_key}"
|
90
|
+
}
|
91
|
+
) do |f|
|
92
|
+
f.request :json
|
93
|
+
f.request :retry
|
94
|
+
f.response :raise_error
|
95
|
+
f.response :json
|
96
|
+
f.adapter :net_http
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_magic_link(
|
102
|
+
user_id: '',
|
103
|
+
email: '',
|
104
|
+
phone: '',
|
105
|
+
channel: '',
|
106
|
+
send: false,
|
107
|
+
magic_link_path: '',
|
108
|
+
redirect_url: '',
|
109
|
+
ttl: 60
|
110
|
+
)
|
111
|
+
magic_link_req = {}
|
112
|
+
magic_link_req['user_id'] = user_id unless user_id.empty?
|
113
|
+
magic_link_req['email'] = email unless email.empty?
|
114
|
+
magic_link_req['phone'] = phone unless phone.empty?
|
115
|
+
|
116
|
+
# check to see if the channel specified is valid before sending it off to the server
|
117
|
+
unless [PHONE_CHANNEL, EMAIL_CHANNEL].include? channel
|
118
|
+
raise PassageError,
|
119
|
+
'channel: must be either Passage::EMAIL_CHANNEL or Passage::PHONE_CHANNEL'
|
120
|
+
end
|
121
|
+
magic_link_req['channel'] = channel unless channel.empty?
|
122
|
+
magic_link_req['send'] = send
|
123
|
+
magic_link_req['magic_link_path'] = magic_link_path unless magic_link_path
|
124
|
+
.empty?
|
125
|
+
magic_link_req['redirect_url'] = redirect_url unless redirect_url.empty?
|
126
|
+
magic_link_req['ttl'] = ttl unless ttl == 0
|
127
|
+
|
128
|
+
begin
|
129
|
+
response =
|
130
|
+
@connection.post("/v1/apps/#{@app_id}/magic-links", magic_link_req)
|
131
|
+
magic_link = response.body['magic_link']
|
132
|
+
return(
|
133
|
+
Passage::MagicLink.new(
|
134
|
+
id: magic_link['id'],
|
135
|
+
secret: magic_link['secret'],
|
136
|
+
activated: magic_link['activated'],
|
137
|
+
user_id: magic_link['user_id'],
|
138
|
+
app_id: magic_link['app_id'],
|
139
|
+
identifier: magic_link['identifier'],
|
140
|
+
type: magic_link['type'],
|
141
|
+
redirect_url: magic_link['redirect_url'],
|
142
|
+
ttl: magic_link['ttl'],
|
143
|
+
url: magic_link['url']
|
144
|
+
)
|
145
|
+
)
|
146
|
+
rescue Faraday::Error => e
|
147
|
+
raise PassageError,
|
148
|
+
"failed to create Passage Magic Link. Http Status: #{e.response[:status]}. Response: #{e.response[:body]['error']}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
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['webauthn_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['webauthn_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['webauthn_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['webauthn_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['webauthn_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
|
data/lib/passageidentity.rb
CHANGED
data/passage-ruby
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
passage-ruby
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'passageidentity'
|
3
|
+
s.version = '0.0.4'
|
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
data/tests/auth_test.rb
ADDED
@@ -0,0 +1,14 @@
|
|
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_authenticate_token
|
11
|
+
user_id = PassageClient.auth.authenticate_token(ENV['PSG_JWT'])
|
12
|
+
assert_equal ENV['TEST_USER_ID'], user_id
|
13
|
+
end
|
14
|
+
end
|
@@ -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.
|
4
|
+
version: 0.0.4
|
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-
|
11
|
+
date: 2022-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -65,7 +65,21 @@ executables: []
|
|
65
65
|
extensions: []
|
66
66
|
extra_rdoc_files: []
|
67
67
|
files:
|
68
|
+
- ".gitignore"
|
69
|
+
- CONTRIBUTING.md
|
70
|
+
- LICENSE
|
71
|
+
- README.md
|
68
72
|
- lib/passageidentity.rb
|
73
|
+
- lib/passageidentity/auth.rb
|
74
|
+
- lib/passageidentity/client.rb
|
75
|
+
- lib/passageidentity/error.rb
|
76
|
+
- lib/passageidentity/user_api.rb
|
77
|
+
- passage-ruby
|
78
|
+
- passageidentity.gemspec
|
79
|
+
- tests/all.rb
|
80
|
+
- tests/auth_test.rb
|
81
|
+
- tests/magic_link_test.rb
|
82
|
+
- tests/user_api_test.rb
|
69
83
|
homepage: https://rubygems.org/gems/passageidentity
|
70
84
|
licenses:
|
71
85
|
- MIT
|