authify-api 0.3.2 → 0.3.3

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
  SHA1:
3
- metadata.gz: 034407bd9058f89a81db15e6d658f16f5d5a5365
4
- data.tar.gz: af8a965cc5f481ba754ccd2c5fe79168e35daf68
3
+ metadata.gz: ee72e82c5e6c776b0cfc07e62c480ef59ecf24fe
4
+ data.tar.gz: b3dfd57c8aefb052282db076a842d15d6d75dbae
5
5
  SHA512:
6
- metadata.gz: 3fb4dd7817c46abf6dc0a2342b48281a9f1862123e990f7c9f4af11baa6d408533fad7376eca7740f3fff865c6052ef63efc88f5b4059abdeb0b8c04f25a8375
7
- data.tar.gz: ffafbf61deae31c0ae88c4d8d1333fca27464fd557b5876ddab461304f5fe50a84b31a645da7b948ba55925f9e43a0ce213a0957c77e08542b7e785455f81eb1
6
+ metadata.gz: 1fa30f55edc35e440b3d64522252d28fb90aee4c7b5e3d4c5a5a2df17d9c8e447db3083d8f314d80d01e3054ea1e852ecdd088d9b3fbdeb75e2da9618671f47a
7
+ data.tar.gz: 9695fdfe722fc53c3cd7741cbfdde9767acbe0c0c8981c77140a926b44f3f34e3db34d43df34b1e9211c77880942fc23d56ef21bc9ebe92d1f9455f748831bc1
data/README.md CHANGED
@@ -22,9 +22,9 @@ Nearly all API endpoints available via Authify implement the [{json:api}](http:/
22
22
  * `GET /jwt/key` - Returns Content Type: `application/json`. This endpoint returns a JSON Object with the key `data` whose value is a PEM-encoded ECDSA public key, which should be used to verify the signature made by the Authify service.
23
23
  * `GET /jwt/meta` - Returns Content Type: `application/json`. This endpoint returns a JSON Object with the keys `algorithm`, `issuer`, and `expiration` that describe the kind of JWTs produced by this service.
24
24
  * `POST /jwt/token` - Returns (and only accepts) Content Type: `application/json`. This endpoint is used to obtain a [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token). This endpoint expects a JSON Object with either the keys `access_key` and `secret_key` _OR_ `email` and `password`. There is no firm requirement to use either pair for any particular purpose, but for scenarios where the credentials may be stored, the `access_key` and `secret_key` may be used since those can easily be revoked if necessary. Upon successful authentication, the endpoint provides a JSON Object with the key `jwt` and a signed JWT. There should be nothing highly sensitive embedded in the JWT. The JWT defaults to expiring every 15 minutes. This endpoint also allows optionally specifying a key called `inject` with a JSON object as a value. This JSON object will then be injected into a top-level `custom` key in the returned JWT _as is_.
25
- * `POST /registration/signup` - Returns (and only accepts) Content Type: `application/json`. This endpoint is used to signup for an account with Authify. This endpoint expects a JSON Object, requiring the keys `email` and `password`, with `name` and `via` being optional. If `via` is provided, then it must be a JSON Object with the keys `provider` and `uid`, otherwise it will be ignored. The `via` key is used to add an alternate identity (meaning they logged-in through an integration, like Github), and is only trusted from trusted delegates (meaning it will be ignored for anonymous calls to this endpoint). This endpoint returns a JSON Object with the keys `id`, `email`, and `verified`, on success. If the user is registered by a trusted delegate *and* `via` options were provided, the users is implicitly trusted and a `jwt` key will also be provided for authentication.
25
+ * `POST /registration/signup` - Returns (and only accepts) Content Type: `application/json`. This endpoint is used to signup for an account with Authify. This endpoint expects a JSON Object, requiring the keys `email` and `password`, with `name` and `via` being optional. If `via` is provided, then it must be a JSON Object with the keys `provider` and `uid`, otherwise it will be ignored. The `via` key is used to add an alternate identity (meaning they logged-in through an integration, like Github), and is only trusted from trusted delegates (meaning it will be ignored for anonymous calls to this endpoint). This endpoint returns a JSON Object with the keys `id`, `email`, and `verified`, on success. If the user is registered by a trusted delegate *and* `via` options were provided, the users is implicitly trusted and a `jwt` key will also be provided for authentication. This endpoint allows customization of the emails sent for users requiring verification. For information on how this works, see the [Templating](#templating) section. The following template expressions are available: `token` and `valid_until`.
26
26
  * `POST /registration/verify` - Returns (and only accepts) Content Type: `application/json`. This endpoint is used to verify a registered user's email address. This endpoint expects a JSON Object, requiring the keys `email`, `password`, and `token`. This endpoint returns a JSON Object with the keys `id`, `email`, `verified`, and `jwt` on success.
27
- * `POST /registration/forgot_password` - Returns (and only accepts) Content Type: `application/json`. This endpoint serves two related purposes: it is used to trigger resetting a forgotten (or non-existent) password and it is used to actually set the value of a user's password. The difference in which operation is performed is based on the POST data. When provided a JSON Object with only the key `email`, the endpoint sends the user an email with a verification token, returning an empty JSON Object as a result. When provided a JSON Object with the keys `email`, `password`, and `token`, the endpoint verifies that the token matches, then sets the user's password, returning a JSON Object with the keys `id`, `email`, `verified`, and `jwt` on success.
27
+ * `POST /registration/forgot_password` - Returns (and only accepts) Content Type: `application/json`. This endpoint serves two related purposes: it is used to trigger resetting a forgotten (or non-existent) password and it is used to actually set the value of a user's password. The difference in which operation is performed is based on the POST data. When provided a JSON Object with only the key `email`, the endpoint sends the user an email with a verification token, returning an empty JSON Object as a result. When provided a JSON Object with the keys `email`, `password`, and `token`, the endpoint verifies that the token matches, then sets the user's password, returning a JSON Object with the keys `id`, `email`, `verified`, and `jwt` on success. This endpoint allows customization of the emails sent for users requiring verification. For information on how this works, see the [Templating](#templating) section. The following template expressions are available: `token` and `valid_until`.
28
28
 
29
29
  All other endpoints adhere to the {json:api} specification and can be found at the following base paths:
30
30
 
@@ -109,7 +109,7 @@ curl \
109
109
  '{
110
110
  "name": "Some User",
111
111
  "email": "someuser@mycompany.com",
112
- "password": "b@d!dea",
112
+ "password": "b@d!dea"
113
113
  }' \
114
114
  https://auth.mycompany.com/registration/signup
115
115
  ```
@@ -254,6 +254,70 @@ curl \
254
254
  https://auth.mycompany.com/organizations
255
255
  ```
256
256
 
257
+ ### Templating
258
+
259
+ Some endpoints support custom templates (and other customizations) for communications sent out to users. This is most useful for services that integrate with Authify but wrap that integration in their own UI.
260
+
261
+ If an endpoint declares that it supports templating (such as `/registration/signup`), what this means is that the JSON `POST` data can include an optional `templates` key. To customize the plaintext email body and subject, you can change a `POST` from something like this:
262
+
263
+ ```javascript
264
+ {
265
+ "name": "Some User",
266
+ "email": "someuser@mycompany.com",
267
+ "password": "b@d!dea"
268
+ }
269
+ ```
270
+
271
+ to include a `templates` section like this:
272
+
273
+ ```javascript
274
+ {
275
+ "name": "Some User",
276
+ "email": "someuser@mycompany.com",
277
+ "password": "b@d!dea",
278
+ "templates": {
279
+ "email": {
280
+ "body": "Your code is: '{{token}}' and it is valid until {{valid_until}}.",
281
+ "subject": "Verification Code"
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ Authify's templating supports something that looks a bit like [Handlebars](http://handlebarsjs.com/) templating (though it doesn't yet support most of the Handlebars features). This is useful for allowing the injection of dynamic data into your templates. Available expressions should be declared in the README section that describes a template-capable endpoint.
288
+
289
+ For some template data, escaping can be difficult or inconvenient. For these situations, Authify supports optional [Base64](https://en.wikipedia.org/wiki/Base64) encoding of values. To provide a Base64-encoded value, just declare it as such using `{base64}` followed by the data:
290
+
291
+ ```javascript
292
+ {
293
+ "name": "Some User",
294
+ "email": "someuser@mycompany.com",
295
+ "password": "b@d!dea",
296
+ "templates": {
297
+ "email": {
298
+ "body": "{base64}WW91ciBjb2RlIGlzOiAne3t0b2tlbn19JyBhbmQgaXQgaXMgdmFsaWQgdW50aWwge3t2YWxpZF91bnRpbH19Lg==",
299
+ "subject": "Verification Code"
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
305
+ Encoded template data still supports the Handlebars-style templating, but it must be applied _before_ the content is Base64-encoded.
306
+
307
+ #### Template Types
308
+
309
+ Currently, only email communications can be templated. The following keys are available for email templates:
310
+
311
+ ```javascript
312
+ "templates": {
313
+ "email": {
314
+ "subject": "The subject of the email",
315
+ "body": "The plaintext body of the email",
316
+ "html_body": "<p>An <a href=\"https://en.wikipedia.org/wiki/HTML\">HTML</a> body.</p>"
317
+ }
318
+ }
319
+ ```
320
+
257
321
  ## Contributing
258
322
 
259
323
  Bug reports and pull requests are welcome on GitHub at https://github.com/knuedge/authify-api.
data/lib/authify/api.rb CHANGED
@@ -44,6 +44,9 @@ require 'authify/api/serializers/group_serializer'
44
44
  require 'authify/api/serializers/identity_serializer'
45
45
  require 'authify/api/serializers/user_serializer'
46
46
  require 'authify/api/serializers/organization_serializer'
47
+ require 'authify/api/helpers/jwt_encryption'
48
+ require 'authify/api/helpers/api_user'
49
+ require 'authify/api/helpers/text_processing'
47
50
  require 'authify/api/models/apikey'
48
51
  require 'authify/api/models/group'
49
52
  require 'authify/api/models/identity'
@@ -51,8 +54,6 @@ require 'authify/api/models/organization'
51
54
  require 'authify/api/models/organization_membership'
52
55
  require 'authify/api/models/trusted_delegate'
53
56
  require 'authify/api/models/user'
54
- require 'authify/api/helpers/jwt_encryption'
55
- require 'authify/api/helpers/api_user'
56
57
  require 'authify/api/service'
57
58
  require 'authify/api/metrics'
58
59
  require 'authify/api/middleware/metrics'
@@ -0,0 +1,37 @@
1
+ module Authify
2
+ module API
3
+ module Helpers
4
+ # Helper methods for working with different text formats
5
+ module TextProcessing
6
+ def valid_formats
7
+ [:base64]
8
+ end
9
+
10
+ def decoded_hash(hash)
11
+ hash.update(hash) { |_, v| v.is_a?(String) ? human_readable(v) : v }
12
+ end
13
+
14
+ # Interpolates handlebars-style templates
15
+ # OPTIMIZE: this can probably be faster
16
+ def dehandlebar(text, data = {})
17
+ text.gsub(/{{([a-z0-9_-]+)}}/) { data[Regexp.last_match[1].to_sym].to_s }
18
+ end
19
+
20
+ def human_readable(text)
21
+ if text =~ /^\{([a-zA-Z0-9]+)\}([^\n]+)$/
22
+ form, data = text.match(/^\{([a-zA-Z0-9]+)\}([^\n]+)$/)[1, 2]
23
+
24
+ raise "Invalid Conversion: #{form}" unless valid_formats.include?(form.downcase.to_sym)
25
+ send("from_#{form.downcase}".to_sym, data)
26
+ else
27
+ text
28
+ end
29
+ end
30
+
31
+ def from_base64(text)
32
+ Base64.decode64 text
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -5,15 +5,11 @@ module Authify
5
5
  include Singleton
6
6
 
7
7
  def update(key, value)
8
- write_lock do
9
- storage[key] = value
10
- end
8
+ storage[key] = value
11
9
  end
12
10
 
13
11
  def increment(key, change = 1)
14
- write_lock do
15
- storage.key?(key) ? storage[key] += change : storage[key] = change
16
- end
12
+ storage.key?(key) ? storage[key] += change : storage[key] = change
17
13
  end
18
14
 
19
15
  def decrement(key, change = 1)
@@ -39,13 +35,6 @@ module Authify
39
35
 
40
36
  private
41
37
 
42
- def write_lock
43
- @wlock ||= Mutex.new
44
- @wlock.synchronize do
45
- yield
46
- end
47
- end
48
-
49
38
  def storage
50
39
  # While not perfectly thread-safe, this works well enough for Metrics
51
40
  @storage ||= Concurrent::Map.new
@@ -15,6 +15,7 @@ module Authify
15
15
  end
16
16
 
17
17
  @metrics.increment construct_metric_key('count', env)
18
+ @metrics.increment 'rack.request.count'
18
19
 
19
20
  [status, header, body]
20
21
  end
@@ -5,6 +5,7 @@ module Authify
5
5
  class User < ActiveRecord::Base
6
6
  include Core::SecureHashing
7
7
  include JSONAPIUtils
8
+ include Helpers::TextProcessing
8
9
 
9
10
  attr_reader :password
10
11
 
@@ -49,22 +50,31 @@ module Authify
49
50
  end
50
51
 
51
52
  # Both sets a token in the DB *and* emails it to the user
52
- def set_verification_token!
53
+ def add_verification_token!(opts = {})
53
54
  return false if verified?
54
55
  token = peppered_sha512(rand(999).to_s)[0...16]
55
- valid_until = (Time.now + (15 * 60)).to_i
56
+ valid_time = Time.now + (15 * 60)
57
+ valid_until = valid_time.to_i
56
58
  self.verification_token = "#{token}:#{valid_until}"
57
59
 
60
+ subdata = { token: token, valid_until: valid_time }
61
+
58
62
  email_opts = {
59
- body: "Your verification token is: #{token}"
63
+ body: if opts.key?(:body)
64
+ dehandlebar(opts[:body], subdata)
65
+ else
66
+ "Your verification token is: #{token}"
67
+ end
60
68
  }
61
69
 
62
- Resque.enqueue(
63
- Authify::Core::Jobs::Email,
64
- email,
65
- 'Authify Verification Email',
66
- email_opts
67
- )
70
+ email_opts[:html_body] = dehandlebar(opts[:html_body], subdata) if opts.key?(:html_body)
71
+ subject = if opts.key?(:subject)
72
+ dehandlebar(opts[:subject], subdata)
73
+ else
74
+ 'Authify Verification Email'
75
+ end
76
+
77
+ Resque.enqueue Authify::Core::Jobs::Email, email, subject, email_opts
68
78
  end
69
79
 
70
80
  def admin_for?(organization)
@@ -29,19 +29,40 @@ module Authify
29
29
  end
30
30
  end
31
31
 
32
- before '*' do
33
- # headers 'Access-Control-Allow-Origin' => '*',
34
- # 'Access-Control-Allow-Methods' => %w(
35
- # OPTIONS
36
- # DELETE
37
- # GET
38
- # PATCH
39
- # POST
40
- # PUT
41
- # )
32
+ before do
42
33
  determine_roles
43
34
  end
44
35
 
36
+ after do
37
+ headers 'Access-Control-Allow-Origin' => '*',
38
+ 'Access-Control-Allow-Methods' => response['Allow'] || %w[
39
+ OPTIONS
40
+ GET
41
+ POST
42
+ PUT
43
+ PATCH
44
+ DELETE
45
+ HEAD
46
+ ],
47
+ 'Access-Control-Allow-Headers' => %w[
48
+ Origin
49
+ Accept
50
+ Accept-Encoding
51
+ Accept-Language
52
+ Access-Control-Request-Headers
53
+ Access-Control-Request-Method
54
+ Authorization
55
+ Connection
56
+ Content-Type
57
+ Host
58
+ Referer
59
+ User-Agent
60
+ X-Requested-With
61
+ X-Forwarded-For
62
+ X-XSRF-Token
63
+ ]
64
+ end
65
+
45
66
  resource :apikeys, &Controllers::APIKey
46
67
  resource :identities, &Controllers::Identity
47
68
  resource :groups, &Controllers::Group
@@ -11,14 +11,22 @@ module Authify
11
11
  set :protection, except: :http_origin
12
12
  end
13
13
 
14
- before '*' do
14
+ before do
15
15
  content_type 'application/json'
16
+
17
+ begin
18
+ unless request.get? || request.options?
19
+ request.body.rewind
20
+ @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
21
+ end
22
+ rescue => e
23
+ halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
24
+ end
25
+ end
26
+
27
+ after do
16
28
  headers 'Access-Control-Allow-Origin' => '*',
17
- 'Access-Control-Allow-Methods' => %w[
18
- OPTIONS
19
- GET
20
- POST
21
- ],
29
+ 'Access-Control-Allow-Methods' => %w[OPTIONS GET POST],
22
30
  'Access-Control-Allow-Headers' => %w[
23
31
  Origin
24
32
  Accept
@@ -26,6 +34,7 @@ module Authify
26
34
  Accept-Language
27
35
  Access-Control-Request-Headers
28
36
  Access-Control-Request-Method
37
+ Authorization
29
38
  Connection
30
39
  Content-Type
31
40
  Host
@@ -33,16 +42,8 @@ module Authify
33
42
  User-Agent
34
43
  X-Requested-With
35
44
  X-Forwarded-For
45
+ X-XSRF-Token
36
46
  ]
37
-
38
- begin
39
- unless request.get? || request.options?
40
- request.body.rewind
41
- @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
42
- end
43
- rescue => e
44
- halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
45
- end
46
47
  end
47
48
 
48
49
  post '/token' do
@@ -7,14 +7,22 @@ module Authify
7
7
  set :protection, except: :http_origin
8
8
  end
9
9
 
10
- before '*' do
10
+ before do
11
11
  content_type 'application/json'
12
+
13
+ begin
14
+ unless request.get? || request.options?
15
+ request.body.rewind
16
+ @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
17
+ end
18
+ rescue => e
19
+ halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
20
+ end
21
+ end
22
+
23
+ after do
12
24
  headers 'Access-Control-Allow-Origin' => '*',
13
- 'Access-Control-Allow-Methods' => %w[
14
- OPTIONS
15
- GET
16
- POST
17
- ],
25
+ 'Access-Control-Allow-Methods' => %w[OPTIONS GET POST],
18
26
  'Access-Control-Allow-Headers' => %w[
19
27
  Origin
20
28
  Accept
@@ -22,6 +30,7 @@ module Authify
22
30
  Accept-Language
23
31
  Access-Control-Request-Headers
24
32
  Access-Control-Request-Method
33
+ Authorization
25
34
  Connection
26
35
  Content-Type
27
36
  Host
@@ -29,16 +38,8 @@ module Authify
29
38
  User-Agent
30
39
  X-Requested-With
31
40
  X-Forwarded-For
41
+ X-XSRF-Token
32
42
  ]
33
-
34
- begin
35
- unless request.get? || request.options?
36
- request.body.rewind
37
- @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
38
- end
39
- rescue => e
40
- halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
41
- end
42
43
  end
43
44
 
44
45
  # Provide information about the JWTs generated by the server
@@ -5,19 +5,28 @@ module Authify
5
5
  class Registration < Service
6
6
  use Authify::API::Middleware::Metrics
7
7
  helpers Helpers::APIUser
8
+ helpers Helpers::TextProcessing
8
9
 
9
10
  configure do
10
11
  set :protection, except: :http_origin
11
12
  end
12
13
 
13
- before '*' do
14
+ before do
14
15
  content_type 'application/json'
16
+
17
+ begin
18
+ unless request.get? || request.options?
19
+ request.body.rewind
20
+ @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
21
+ end
22
+ rescue => e
23
+ halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
24
+ end
25
+ end
26
+
27
+ after do
15
28
  headers 'Access-Control-Allow-Origin' => '*',
16
- 'Access-Control-Allow-Methods' => %w[
17
- OPTIONS
18
- GET
19
- POST
20
- ],
29
+ 'Access-Control-Allow-Methods' => %w[OPTIONS GET POST],
21
30
  'Access-Control-Allow-Headers' => %w[
22
31
  Origin
23
32
  Accept
@@ -25,6 +34,7 @@ module Authify
25
34
  Accept-Language
26
35
  Access-Control-Request-Headers
27
36
  Access-Control-Request-Method
37
+ Authorization
28
38
  Connection
29
39
  Content-Type
30
40
  Host
@@ -32,16 +42,8 @@ module Authify
32
42
  User-Agent
33
43
  X-Requested-With
34
44
  X-Forwarded-For
45
+ X-XSRF-Token
35
46
  ]
36
-
37
- begin
38
- unless request.get? || request.options?
39
- request.body.rewind
40
- @parsed_body = JSON.parse(request.body.read, symbolize_names: true)
41
- end
42
- rescue => e
43
- halt(400, { error: "Request must be valid JSON: #{e.message}" }.to_json)
44
- end
45
47
  end
46
48
 
47
49
  post '/signup' do
@@ -49,6 +51,7 @@ module Authify
49
51
  via = @parsed_body[:via]
50
52
  password = @parsed_body[:password]
51
53
  name = @parsed_body[:name]
54
+ templates = @parsed_body[:templates]
52
55
 
53
56
  halt(422, 'Duplicate User') if Models::User.exists?(email: email)
54
57
  halt(403, 'Password Required') unless password || remote_app
@@ -58,12 +61,13 @@ module Authify
58
61
  new_user.password = password if password
59
62
  if via && via[:provider] && remote_app
60
63
  new_user.identities.build(
61
- provider: via[:provider],
62
- uid: via[:uid] ? via[:uid] : email
64
+ provider: via[:provider], uid: via[:uid] ? via[:uid] : email
63
65
  )
64
66
  new_user.verified = true
67
+ elsif templates && templates.key?(:email)
68
+ new_user.add_verification_token!(decoded_hash(templates[:email]))
65
69
  else
66
- new_user.set_verification_token!
70
+ new_user.add_verification_token!
67
71
  end
68
72
 
69
73
  new_user.save
@@ -86,6 +90,8 @@ module Authify
86
90
  post '/forgot_password' do
87
91
  email = @parsed_body[:email]
88
92
  token = @parsed_body[:token]
93
+ templates = @parsed_body[:templates]
94
+
89
95
  halt(200, '{}') unless Models::User.exists?(email: email)
90
96
  halt(403, 'Missing Parameters') unless email
91
97
 
@@ -103,7 +109,11 @@ module Authify
103
109
  }.to_json
104
110
  else
105
111
  found_user.verified = false
106
- found_user.set_verification_token!
112
+ if templates && templates.key?(:email)
113
+ found_user.add_verification_token!(decoded_hash(templates[:email]))
114
+ else
115
+ found_user.add_verification_token!
116
+ end
107
117
  found_user.save
108
118
  halt(200, '{}')
109
119
  end
@@ -3,7 +3,7 @@ module Authify
3
3
  VERSION = [
4
4
  0, # Major
5
5
  3, # Minor
6
- 2 # Patch
6
+ 3 # Patch
7
7
  ].join('.')
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authify-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-19 00:00:00.000000000 Z
11
+ date: 2017-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: authify-core
@@ -368,6 +368,7 @@ files:
368
368
  - lib/authify/api/controllers/user.rb
369
369
  - lib/authify/api/helpers/api_user.rb
370
370
  - lib/authify/api/helpers/jwt_encryption.rb
371
+ - lib/authify/api/helpers/text_processing.rb
371
372
  - lib/authify/api/jsonapi_utils.rb
372
373
  - lib/authify/api/metrics.rb
373
374
  - lib/authify/api/middleware/metrics.rb