aha_builder_core 1.0.15 → 1.0.16
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/README.md +23 -3
- data/lib/aha/auth/client.rb +18 -6
- data/lib/aha/auth/configuration.rb +8 -0
- data/lib/aha/auth/token_cache.rb +7 -5
- data/lib/aha/auth/users_resource.rb +3 -3
- data/lib/aha/auth.rb +6 -19
- data/lib/aha/mail/delivery_method.rb +66 -0
- data/lib/aha/mail.rb +19 -0
- data/lib/aha/version.rb +1 -1
- data/lib/aha_builder_core.rb +1 -0
- data/lib/generators/aha_builder_core/auth/USAGE +1 -1
- data/lib/generators/aha_builder_core/auth/templates/authentication.rb.tt +2 -4
- data/lib/generators/aha_builder_core/auth/templates/sessions_controller.rb.tt +11 -3
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 040e466bb6704c03036a10c3b1242abe7180212e6c34319ff6377d090f1f5373
|
|
4
|
+
data.tar.gz: 8fae9ac629308b1f7ecf306f3f5fee5b362f181fab5dd2a2f75502e0ed22dccf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16711e2de018b08fdedcb47a92d5e1a6a3ea3862d88d786c8ce609d76de9f620293760cbb74bb7cff68d243133ee0ad6041020c02e98ee7d6210d68ea234b7a3
|
|
7
|
+
data.tar.gz: 883be935bafe88f56091079f1b38cdcc3104b115d3f1d3e633bb78d501e9c0b9d7df9b3010416162343b7c943623eb124645bbf2282dc8336b5be2c691308534
|
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Aha Builder Core Client
|
|
2
2
|
|
|
3
|
-
Ruby client for Aha! Builder core
|
|
3
|
+
Ruby client for Aha! Builder core which provides application services:
|
|
4
|
+
|
|
5
|
+
* Authentication services for login and signup using email/password, social logins (Google, Github, Microsoft), SAML SSO and password reset.
|
|
6
|
+
* Email sending API.
|
|
7
|
+
* User guide content API.
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
@@ -18,7 +22,7 @@ No configuration is necessary and no environment variables are necessary.
|
|
|
18
22
|
|
|
19
23
|
The authentication UI is provided completely by the core system. During authentication the user is redirected to the login page, and will return (via HTTP redirect) to the `/callback` URL when authentication is complete. Your application must implement a callback action at `/callback` to receive the code. Any value passed in as `state` is returned verbatim to the callback.
|
|
20
24
|
|
|
21
|
-
Protection against CSRF
|
|
25
|
+
Protection against CSRF attacks is handled internally by the Aha::Auth library and there is no need to use a nonce in the state parameter.
|
|
22
26
|
|
|
23
27
|
### Generate Login URL
|
|
24
28
|
|
|
@@ -88,7 +92,6 @@ local_user.update!(
|
|
|
88
92
|
# Store tokens in session or database
|
|
89
93
|
session[:session_token] = result[:session_token]
|
|
90
94
|
session[:refresh_token] = result[:refresh_token]
|
|
91
|
-
session[:user_id] = local_user.id
|
|
92
95
|
|
|
93
96
|
# Redirect to application
|
|
94
97
|
redirect_to root_path
|
|
@@ -202,3 +205,20 @@ Each page hash contains:
|
|
|
202
205
|
- `position` - Sort order (integer)
|
|
203
206
|
- `children_count` - Number of child pages
|
|
204
207
|
- `content_html` - HTML content (only in `load_page` response)
|
|
208
|
+
|
|
209
|
+
## Email Sending
|
|
210
|
+
|
|
211
|
+
Aha! Builder core provides a mail API that can be used as an ActionMailer delivery method.
|
|
212
|
+
|
|
213
|
+
### ActionMailer Integration
|
|
214
|
+
|
|
215
|
+
Configure your Rails application to send emails through Aha! Builder core:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# config/initializers/action_mailer.rb
|
|
219
|
+
ActionMailer::Base.add_delivery_method :aha_mail, Aha::Mail::DeliveryMethod
|
|
220
|
+
|
|
221
|
+
# config/environments/production.rb
|
|
222
|
+
config.action_mailer.delivery_method = :aha_mail
|
|
223
|
+
```
|
|
224
|
+
|
data/lib/aha/auth/client.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Aha
|
|
|
52
52
|
begin
|
|
53
53
|
claims = decode_and_verify_token(session_token)
|
|
54
54
|
return Session.from_claims(claims) if claims
|
|
55
|
-
rescue ExpiredTokenError
|
|
55
|
+
rescue ExpiredTokenError
|
|
56
56
|
# Token has expired, attempt refresh
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -73,7 +73,8 @@ module Aha
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
Session.invalid
|
|
76
|
-
rescue InvalidTokenError, ExpiredTokenError
|
|
76
|
+
rescue InvalidTokenError, ExpiredTokenError => e
|
|
77
|
+
Rails.logger.info("Session token invalid or expired #{e.message}")
|
|
77
78
|
Session.invalid
|
|
78
79
|
end
|
|
79
80
|
|
|
@@ -82,9 +83,12 @@ module Aha
|
|
|
82
83
|
# @param session_token [String] The session token to revoke
|
|
83
84
|
# @return [Boolean] true if successful
|
|
84
85
|
def logout(session_token:)
|
|
85
|
-
|
|
86
|
+
claims = decode_and_verify_token(session_token)
|
|
87
|
+
user_id = claims["sub"] # JWT subject is the user_id
|
|
88
|
+
|
|
89
|
+
post("/api/core/auth/users/#{user_id}/logout", {})
|
|
86
90
|
true
|
|
87
|
-
rescue ApiError
|
|
91
|
+
rescue InvalidTokenError, ExpiredTokenError, ApiError
|
|
88
92
|
false
|
|
89
93
|
end
|
|
90
94
|
|
|
@@ -106,7 +110,7 @@ module Aha
|
|
|
106
110
|
public_key = @token_cache.get_key(kid) { fetch_jwks }
|
|
107
111
|
|
|
108
112
|
unless public_key
|
|
109
|
-
#
|
|
113
|
+
# Force refresh the cache in case there's a new key
|
|
110
114
|
@token_cache.refresh! { fetch_jwks }
|
|
111
115
|
public_key = @token_cache.get_key(kid) { fetch_jwks }
|
|
112
116
|
raise InvalidTokenError, "Unknown key ID: #{kid}" unless public_key
|
|
@@ -115,7 +119,11 @@ module Aha
|
|
|
115
119
|
# Verify the token
|
|
116
120
|
options = {
|
|
117
121
|
algorithm: ALGORITHM,
|
|
118
|
-
|
|
122
|
+
aud: @configuration.client_id.to_s,
|
|
123
|
+
verify_expiration: true,
|
|
124
|
+
verify_aud: true,
|
|
125
|
+
verify_iat: true,
|
|
126
|
+
required_claims: %w[sub sid aud iat exp],
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
decoded = JWT.decode(token, public_key, true, options)
|
|
@@ -157,6 +165,10 @@ module Aha
|
|
|
157
165
|
|
|
158
166
|
def http_client
|
|
159
167
|
@http_client ||= Faraday.new(url: @configuration.server_url) do |conn|
|
|
168
|
+
if @configuration.enable_logging && @configuration.logger
|
|
169
|
+
conn.response :logger, @configuration.logger, bodies: true
|
|
170
|
+
end
|
|
171
|
+
|
|
160
172
|
conn.request :retry, max: 2, interval: 0.5, backoff_factor: 2
|
|
161
173
|
conn.options.timeout = @configuration.timeout
|
|
162
174
|
|
|
@@ -21,6 +21,12 @@ module Aha
|
|
|
21
21
|
# HTTP timeout in seconds (default: 30)
|
|
22
22
|
attr_accessor :timeout
|
|
23
23
|
|
|
24
|
+
# Enable logging of HTTP requests and responses (default: false)
|
|
25
|
+
attr_accessor :enable_logging
|
|
26
|
+
|
|
27
|
+
# Logger instance for HTTP logging (default: Rails.logger)
|
|
28
|
+
attr_accessor :logger
|
|
29
|
+
|
|
24
30
|
def initialize
|
|
25
31
|
@server_url = ENV.fetch("AHA_CORE_SERVER_URL", "https://secure.aha.io/api/core")
|
|
26
32
|
@api_key = ENV.fetch("AHA_CORE_API_KEY", nil)
|
|
@@ -28,6 +34,8 @@ module Aha
|
|
|
28
34
|
@jwks_cache_ttl = 3600 # 1 hour
|
|
29
35
|
@refresh_threshold = 120 # 2 minutes
|
|
30
36
|
@timeout = 30
|
|
37
|
+
@enable_logging = false
|
|
38
|
+
@logger = Rails.logger
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def validate!
|
data/lib/aha/auth/token_cache.rb
CHANGED
|
@@ -34,11 +34,6 @@ module Aha
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Check if the cache is stale
|
|
38
|
-
def stale?
|
|
39
|
-
@fetched_at.nil? || (Time.now.utc - @fetched_at) > @ttl
|
|
40
|
-
end
|
|
41
|
-
|
|
42
37
|
# Clear the cache
|
|
43
38
|
def clear!
|
|
44
39
|
@mutex.synchronize do
|
|
@@ -49,6 +44,10 @@ module Aha
|
|
|
49
44
|
|
|
50
45
|
private
|
|
51
46
|
|
|
47
|
+
def stale?
|
|
48
|
+
@fetched_at.nil? || (Time.now.utc - @fetched_at) > @ttl
|
|
49
|
+
end
|
|
50
|
+
|
|
52
51
|
def refresh_if_needed(&fetcher)
|
|
53
52
|
fetch_keys(&fetcher) if stale?
|
|
54
53
|
end
|
|
@@ -59,7 +58,10 @@ module Aha
|
|
|
59
58
|
|
|
60
59
|
@keys = {}
|
|
61
60
|
jwks["keys"].each do |key_data|
|
|
61
|
+
# Valdate the key is appropriate for our use.
|
|
62
62
|
next unless key_data["kty"] == "RSA"
|
|
63
|
+
next unless key_data["use"] == "sig"
|
|
64
|
+
next unless key_data["alg"] == "RS256"
|
|
63
65
|
|
|
64
66
|
kid = key_data["kid"]
|
|
65
67
|
@keys[kid] = build_rsa_key(key_data)
|
|
@@ -11,9 +11,9 @@ module Aha
|
|
|
11
11
|
# Create a new user
|
|
12
12
|
#
|
|
13
13
|
# @param email [String] User's email address
|
|
14
|
-
# @param first_name [String] User's first name
|
|
15
|
-
# @param last_name [String] User's last name
|
|
16
|
-
# @param password [String] User's password
|
|
14
|
+
# @param first_name [String] User's first name (optional)
|
|
15
|
+
# @param last_name [String] User's last name (optional)
|
|
16
|
+
# @param password [String] User's password (optional)
|
|
17
17
|
# @return [User] The created user
|
|
18
18
|
def create(email:, first_name:, last_name:, password:)
|
|
19
19
|
response = post(
|
data/lib/aha/auth.rb
CHANGED
|
@@ -77,26 +77,13 @@ module Aha
|
|
|
77
77
|
def authenticate_with_code(code:, cookies:)
|
|
78
78
|
# Split the code and nonce if present
|
|
79
79
|
actual_code, nonce = code.split(".", 2)
|
|
80
|
+
|
|
81
|
+
raise "CSRF verification failed: unable to verify nonce" unless nonce
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Verify nonce matches
|
|
86
|
-
if cookie_nonce.blank?
|
|
87
|
-
raise "CSRF verification failed: nonce missing in cookie"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
if cookie_nonce != nonce
|
|
91
|
-
raise "CSRF verification failed: nonce mismatch"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Clear the nonce from session after verification
|
|
95
|
-
cookies.delete(:auth_nonce)
|
|
96
|
-
else
|
|
97
|
-
# If we fon't have a none, we can't verify CSRF.
|
|
98
|
-
raise "CSRF verification failed: unable to verify nonce"
|
|
99
|
-
end
|
|
83
|
+
cookie_nonce = cookies[:auth_nonce]
|
|
84
|
+
cookies.delete(:auth_nonce)
|
|
85
|
+
raise "CSRF verification failed: nonce missing in cookie" if cookie_nonce.blank?
|
|
86
|
+
raise "CSRF verification failed: nonce mismatch" if cookie_nonce != nonce
|
|
100
87
|
|
|
101
88
|
client.authenticate_with_code(code: actual_code)
|
|
102
89
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aha
|
|
4
|
+
module Mail
|
|
5
|
+
class DeliveryMethod
|
|
6
|
+
attr_accessor :settings
|
|
7
|
+
|
|
8
|
+
def initialize(settings = {})
|
|
9
|
+
@settings = settings
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def deliver!(mail)
|
|
13
|
+
# Extract mail attributes
|
|
14
|
+
mail_params = {
|
|
15
|
+
from: format_addresses(mail.from).first,
|
|
16
|
+
to: format_addresses(mail.to),
|
|
17
|
+
cc: format_addresses(mail.cc),
|
|
18
|
+
bcc: format_addresses(mail.bcc),
|
|
19
|
+
reply_to: format_addresses(mail.reply_to),
|
|
20
|
+
subject: mail.subject,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Determine content type and body
|
|
24
|
+
if mail.multipart?
|
|
25
|
+
# For multipart messages, prefer HTML over plain text
|
|
26
|
+
html_part = mail.html_part
|
|
27
|
+
text_part = mail.text_part
|
|
28
|
+
|
|
29
|
+
if html_part
|
|
30
|
+
mail_params[:body] = html_part.body.decoded
|
|
31
|
+
mail_params[:content_type] = "text/html"
|
|
32
|
+
elsif text_part
|
|
33
|
+
mail_params[:body] = text_part.body.decoded
|
|
34
|
+
mail_params[:content_type] = "text/plain"
|
|
35
|
+
else
|
|
36
|
+
mail_params[:body] = mail.body.decoded
|
|
37
|
+
mail_params[:content_type] = mail.content_type
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
mail_params[:body] = mail.body.decoded
|
|
41
|
+
mail_params[:content_type] = mail.content_type || "text/plain"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Send via the Aha::Mail API
|
|
45
|
+
Aha::Mail.send_mail(mail_params)
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
# ActionMailer expects delivery methods to raise exceptions on failure
|
|
48
|
+
raise "Failed to deliver mail via Aha::Mail: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def format_addresses(addresses)
|
|
54
|
+
return [] if addresses.nil?
|
|
55
|
+
|
|
56
|
+
Array(addresses).map do |address|
|
|
57
|
+
if address.is_a?(::Mail::Address)
|
|
58
|
+
address.to_s
|
|
59
|
+
else
|
|
60
|
+
address
|
|
61
|
+
end
|
|
62
|
+
end.compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/aha/mail.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mail/delivery_method"
|
|
4
|
+
|
|
5
|
+
module Aha
|
|
6
|
+
module Mail
|
|
7
|
+
class << self
|
|
8
|
+
def send_mail(mail_params)
|
|
9
|
+
client.send(:post, "/api/core/mail/send", mail: mail_params)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def client
|
|
15
|
+
Aha::Auth.send(:client)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/aha/version.rb
CHANGED
data/lib/aha_builder_core.rb
CHANGED
|
@@ -16,7 +16,7 @@ Example:
|
|
|
16
16
|
And add routes:
|
|
17
17
|
get "login" => "sessions#new"
|
|
18
18
|
get "callback" => "sessions#callback"
|
|
19
|
-
|
|
19
|
+
get "logout" => "sessions#logout"
|
|
20
20
|
|
|
21
21
|
You can add custom attributes to the User model:
|
|
22
22
|
bin/rails generate aha_builder_core:auth company:string role:string
|
|
@@ -23,7 +23,7 @@ module Authentication
|
|
|
23
23
|
session[:refresh_token] = session_result.new_refresh_token
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
@current_user = User.find_by(id:
|
|
26
|
+
@current_user = User.find_by(id: session_result.user_id)
|
|
27
27
|
Current.user = @current_user
|
|
28
28
|
else
|
|
29
29
|
clear_session
|
|
@@ -50,9 +50,7 @@ module Authentication
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def clear_session
|
|
53
|
-
|
|
54
|
-
session.delete(:refresh_token)
|
|
55
|
-
session.delete(:user_id)
|
|
53
|
+
reset_session
|
|
56
54
|
@current_user = nil
|
|
57
55
|
end
|
|
58
56
|
end
|
|
@@ -4,7 +4,7 @@ class SessionsController < ApplicationController
|
|
|
4
4
|
|
|
5
5
|
def new
|
|
6
6
|
redirect_to Aha::Auth.login_url(
|
|
7
|
-
state: { return_to: params[:return_to] || root_path }.to_json,
|
|
7
|
+
state: { return_to: relative_url(params[:return_to] || root_path) }.to_json,
|
|
8
8
|
cookies:
|
|
9
9
|
), allow_other_host: true
|
|
10
10
|
end
|
|
@@ -25,10 +25,9 @@ class SessionsController < ApplicationController
|
|
|
25
25
|
|
|
26
26
|
session[:session_token] = result[:session_token]
|
|
27
27
|
session[:refresh_token] = result[:refresh_token]
|
|
28
|
-
session[:user_id] = user.id
|
|
29
28
|
|
|
30
29
|
state = JSON.parse(params[:state]) rescue {}
|
|
31
|
-
redirect_to state["return_to"] || root_path
|
|
30
|
+
redirect_to relative_url(state["return_to"] || root_path)
|
|
32
31
|
else
|
|
33
32
|
redirect_to login_path, alert: "Authentication failed. Please try again."
|
|
34
33
|
end
|
|
@@ -49,4 +48,13 @@ class SessionsController < ApplicationController
|
|
|
49
48
|
clear_session
|
|
50
49
|
redirect_to root_path, notice: "Successfully signed out."
|
|
51
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def relative_url(url)
|
|
55
|
+
uri = URI.parse(url)
|
|
56
|
+
[[uri.path, uri.query].compact.join("?"), uri.fragment].compact.join("#")
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
52
60
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aha_builder_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aha! Labs Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -123,6 +123,8 @@ files:
|
|
|
123
123
|
- lib/aha/auth/token_cache.rb
|
|
124
124
|
- lib/aha/auth/user.rb
|
|
125
125
|
- lib/aha/auth/users_resource.rb
|
|
126
|
+
- lib/aha/mail.rb
|
|
127
|
+
- lib/aha/mail/delivery_method.rb
|
|
126
128
|
- lib/aha/user_guide.rb
|
|
127
129
|
- lib/aha/version.rb
|
|
128
130
|
- lib/aha_builder_core.rb
|
|
@@ -149,7 +151,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
149
151
|
requirements:
|
|
150
152
|
- - ">="
|
|
151
153
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: 3.
|
|
154
|
+
version: 3.3.0
|
|
153
155
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
156
|
requirements:
|
|
155
157
|
- - ">="
|