aha_builder_core 1.0.1 → 1.0.3
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 +14 -26
- data/lib/aha/auth/client.rb +15 -18
- data/lib/aha/auth/configuration.rb +1 -1
- data/lib/aha/auth/sessions_resource.rb +2 -0
- data/lib/aha/auth/user.rb +2 -0
- data/lib/aha/auth/version.rb +1 -1
- data/lib/aha/auth.rb +2 -4
- metadata +16 -8
- data/lib/generators/aha_builder_core/authentication/USAGE +0 -21
- data/lib/generators/aha_builder_core/authentication/authentication_generator.rb +0 -57
- data/lib/generators/aha_builder_core/authentication/templates/authentication.rb.erb +0 -48
- data/lib/generators/aha_builder_core/authentication/templates/current.rb.erb +0 -4
- data/lib/generators/aha_builder_core/authentication/templates/sessions_controller.rb.erb +0 -44
- data/lib/generators/aha_builder_core/authentication/templates/user.rb.erb +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bfa1fb612c68e9997d4058381ef9c4b6b8d7eb61ddcbb52800fac41ace7fcd0
|
|
4
|
+
data.tar.gz: e673fd8edc82b0ae0f1a7c32ecf82bdd6c8368ebe2b8b626dc4e5d1cf6d6b696
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2b43eeff2fbea68a9b7b908097ca88bf858504035671808c8dac2716b0621d37219c06f92aaed8d9be7eb6571da665a1f8fed95f52cf02f3d02ba4151c29898
|
|
7
|
+
data.tar.gz: 927e0f48d3a1d2e4cb492ccefa7cb3bc377468ac7c57b3896abcaaa449a3b85a42cad99392927f0733127b28a7fe612d0f365c55060993b3c545c8c193048606
|
data/README.md
CHANGED
|
@@ -12,20 +12,7 @@ gem "aha_builder_core", path: "engines/builder_core/client"
|
|
|
12
12
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
require "aha_builder_core"
|
|
19
|
-
|
|
20
|
-
Aha::Auth.configure do |config|
|
|
21
|
-
config.server_url = "https://secure.aha.io" # Auth server URL
|
|
22
|
-
config.client_id = "your-client-id" # ID of the builder application
|
|
23
|
-
config.api_key = "your-api-key" # For server-to-server auth
|
|
24
|
-
config.jwks_cache_ttl = 3600 # JWKS cache duration (default: 1 hour)
|
|
25
|
-
config.refresh_threshold = 120 # Seconds before expiry to refresh (default: 2 min)
|
|
26
|
-
config.timeout = 30 # HTTP timeout in seconds
|
|
27
|
-
end
|
|
28
|
-
```
|
|
15
|
+
No configuration is necessary and no environment variables are necessary.
|
|
29
16
|
|
|
30
17
|
## Authentication Flow
|
|
31
18
|
|
|
@@ -38,11 +25,12 @@ Redirect users to the authentication and signup UI:
|
|
|
38
25
|
```ruby
|
|
39
26
|
url = Aha::Auth.login_url(
|
|
40
27
|
state: { return_to: "/" }.to_json,
|
|
41
|
-
redirect_uri: "#{ENV['APPLICATION_URL']}/callback"
|
|
42
28
|
)
|
|
43
29
|
redirect_to url
|
|
44
30
|
```
|
|
45
31
|
|
|
32
|
+
Redirecting to the login_url must be done with a full page load. It will not work using XHR.
|
|
33
|
+
|
|
46
34
|
### Exchange Authorization Code
|
|
47
35
|
|
|
48
36
|
In the `/callback` action callback, you must exchange the code for a session token and refresh token:
|
|
@@ -54,11 +42,11 @@ result = Aha::Auth.authenticate_with_code(code: params[:code])
|
|
|
54
42
|
# refresh_token: "...",
|
|
55
43
|
# expires_at: Time,
|
|
56
44
|
# user: {
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
45
|
+
# id: "user-uuid",
|
|
46
|
+
# first_name: "Jane",
|
|
47
|
+
# last_name: "Doe",
|
|
48
|
+
# email: "jane.doe@example.com",
|
|
49
|
+
# email_verified: true
|
|
62
50
|
# }
|
|
63
51
|
# }
|
|
64
52
|
```
|
|
@@ -68,8 +56,8 @@ result = Aha::Auth.authenticate_with_code(code: params[:code])
|
|
|
68
56
|
The `user` object returned contains the authenticated user's profile information from the Builder Core system:
|
|
69
57
|
|
|
70
58
|
- `id`: The unique identifier for the user in Builder Core
|
|
71
|
-
- `first_name`: User's first name
|
|
72
|
-
- `last_name`: User's last name
|
|
59
|
+
- `first_name`: User's first name. first_name is optional and may not be present.
|
|
60
|
+
- `last_name`: User's last name. last_name is optional and may not be present.
|
|
73
61
|
- `email`: User's email address
|
|
74
62
|
- `email_verified`: Boolean indicating if the email has been verified
|
|
75
63
|
|
|
@@ -86,10 +74,10 @@ local_user = User.find_or_initialize_by(auth_identifier: result[:user]["id"])
|
|
|
86
74
|
|
|
87
75
|
# Update local user attributes
|
|
88
76
|
local_user.update!(
|
|
89
|
-
email: result[:user][
|
|
90
|
-
first_name: result[:user][
|
|
91
|
-
last_name: result[:user][
|
|
92
|
-
email_verified: result[:user][
|
|
77
|
+
email: result[:user][:email],
|
|
78
|
+
first_name: result[:user][:first_name],
|
|
79
|
+
last_name: result[:user][:last_name],
|
|
80
|
+
email_verified: result[:user][:email_verified]
|
|
93
81
|
)
|
|
94
82
|
|
|
95
83
|
# Store tokens in session or database
|
data/lib/aha/auth/client.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
|
|
3
5
|
module Aha
|
|
4
6
|
module Auth
|
|
5
7
|
# HTTP client for communicating with the BuilderCore auth server
|
|
@@ -47,14 +49,16 @@ module Aha
|
|
|
47
49
|
# @param refresh_token [String, nil] Optional refresh token for automatic refresh
|
|
48
50
|
# @return [Session] Session validation result
|
|
49
51
|
def validate_session(session_token, refresh_token: nil)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
begin
|
|
53
|
+
claims = decode_and_verify_token(session_token)
|
|
54
|
+
return Session.from_claims(claims) if claims
|
|
55
|
+
rescue ExpiredTokenError
|
|
56
|
+
# Token has expired, attempt refresh
|
|
57
|
+
end
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
exp = claims["exp"]
|
|
55
|
-
if exp && refresh_token && should_refresh?(exp)
|
|
59
|
+
if refresh_token
|
|
56
60
|
begin
|
|
57
|
-
tokens =
|
|
61
|
+
tokens = authenticate_with_refresh_token(refresh_token: refresh_token)
|
|
58
62
|
new_claims = decode_and_verify_token(tokens[:session_token])
|
|
59
63
|
|
|
60
64
|
return Session.from_claims(
|
|
@@ -64,13 +68,11 @@ module Aha
|
|
|
64
68
|
new_refresh_token: tokens[:refresh_token]
|
|
65
69
|
)
|
|
66
70
|
rescue Error
|
|
67
|
-
|
|
68
|
-
# Refresh failed, return session with original token if still valid
|
|
69
|
-
return Session.invalid if Time.at(exp).utc < Time.now.utc
|
|
71
|
+
return Session.invalid
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
|
|
73
|
-
Session.
|
|
75
|
+
Session.invalid
|
|
74
76
|
rescue InvalidTokenError, ExpiredTokenError
|
|
75
77
|
Session.invalid
|
|
76
78
|
end
|
|
@@ -80,7 +82,7 @@ module Aha
|
|
|
80
82
|
# @param session_token [String] The session token to revoke
|
|
81
83
|
# @return [Boolean] true if successful
|
|
82
84
|
def logout(session_token:)
|
|
83
|
-
get("/api/core/
|
|
85
|
+
get("/api/core/auth_ui/logout", headers: { "Authorization" => "Bearer #{session_token}" })
|
|
84
86
|
true
|
|
85
87
|
rescue ApiError
|
|
86
88
|
false
|
|
@@ -90,8 +92,7 @@ module Aha
|
|
|
90
92
|
#
|
|
91
93
|
# @return [Hash] The JWKS response
|
|
92
94
|
def fetch_jwks
|
|
93
|
-
|
|
94
|
-
handle_response(response)
|
|
95
|
+
get("/api/core/auth/jwks/#{@configuration.client_id}")
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
private
|
|
@@ -125,17 +126,13 @@ module Aha
|
|
|
125
126
|
raise InvalidTokenError, "Invalid token: #{e.message}"
|
|
126
127
|
end
|
|
127
128
|
|
|
128
|
-
def should_refresh?(exp)
|
|
129
|
-
Time.at(exp).utc - Time.now.utc <= @configuration.refresh_threshold
|
|
130
|
-
end
|
|
131
|
-
|
|
132
129
|
def parse_token_response(response)
|
|
133
130
|
{
|
|
134
131
|
session_token: response["session_token"],
|
|
135
132
|
refresh_token: response["refresh_token"],
|
|
136
133
|
expires_at: response["expires_at"] ? Time.iso8601(response["expires_at"]) : nil,
|
|
137
134
|
user: response["user"]
|
|
138
|
-
}
|
|
135
|
+
}.with_indifferent_access
|
|
139
136
|
end
|
|
140
137
|
|
|
141
138
|
def get(path, headers: {})
|
|
@@ -22,7 +22,7 @@ module Aha
|
|
|
22
22
|
attr_accessor :timeout
|
|
23
23
|
|
|
24
24
|
def initialize
|
|
25
|
-
@server_url = ENV.fetch("AHA_CORE_SERVER_URL",
|
|
25
|
+
@server_url = ENV.fetch("AHA_CORE_SERVER_URL", "https://secure.aha.io/api/core")
|
|
26
26
|
@api_key = ENV.fetch("AHA_CORE_API_KEY", nil)
|
|
27
27
|
@client_id = ENV.fetch("APPLICATION_ID", nil)
|
|
28
28
|
@jwks_cache_ttl = 3600 # 1 hour
|
data/lib/aha/auth/user.rb
CHANGED
data/lib/aha/auth/version.rb
CHANGED
data/lib/aha/auth.rb
CHANGED
|
@@ -41,15 +41,13 @@ module Aha
|
|
|
41
41
|
# Generate login URL for redirecting users to the auth server
|
|
42
42
|
#
|
|
43
43
|
# @param state [String] Optional state parameter to pass through the auth flow
|
|
44
|
-
# @param redirect_uri [String] Optional redirect URI after authentication
|
|
45
44
|
# @return [String] The login URL
|
|
46
|
-
def login_url(state: nil
|
|
45
|
+
def login_url(state: nil)
|
|
47
46
|
params = { client_id: configuration.client_id }
|
|
48
47
|
params[:state] = state if state
|
|
49
|
-
params[:redirect_uri] = redirect_uri if redirect_uri
|
|
50
48
|
|
|
51
49
|
query = URI.encode_www_form(params)
|
|
52
|
-
"#{configuration.server_url}/
|
|
50
|
+
"#{configuration.server_url}/auth_ui/start?#{query}"
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
# Exchange an authorization code for tokens
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
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.3
|
|
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:
|
|
11
|
+
date: 2026-01-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: concurrent-ruby
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -111,12 +125,6 @@ files:
|
|
|
111
125
|
- lib/aha/auth/users_resource.rb
|
|
112
126
|
- lib/aha/auth/version.rb
|
|
113
127
|
- lib/aha_builder_core.rb
|
|
114
|
-
- lib/generators/aha_builder_core/authentication/USAGE
|
|
115
|
-
- lib/generators/aha_builder_core/authentication/authentication_generator.rb
|
|
116
|
-
- lib/generators/aha_builder_core/authentication/templates/authentication.rb.erb
|
|
117
|
-
- lib/generators/aha_builder_core/authentication/templates/current.rb.erb
|
|
118
|
-
- lib/generators/aha_builder_core/authentication/templates/sessions_controller.rb.erb
|
|
119
|
-
- lib/generators/aha_builder_core/authentication/templates/user.rb.erb
|
|
120
128
|
homepage: https://www.aha.io
|
|
121
129
|
licenses:
|
|
122
130
|
- MIT
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Description:
|
|
2
|
-
Generates authentication setup for Aha! Builder Core client
|
|
3
|
-
|
|
4
|
-
This generator creates:
|
|
5
|
-
- SessionsController with new and callback actions for authentication flow
|
|
6
|
-
- User model for storing authenticated user information
|
|
7
|
-
- Current model for thread-isolated current user access
|
|
8
|
-
- Authentication concern to be included in ApplicationController
|
|
9
|
-
|
|
10
|
-
Example:
|
|
11
|
-
bin/rails generate aha_builder_core:authentication
|
|
12
|
-
|
|
13
|
-
This will create:
|
|
14
|
-
app/controllers/sessions_controller.rb
|
|
15
|
-
app/controllers/concerns/authentication.rb
|
|
16
|
-
app/models/user.rb
|
|
17
|
-
app/models/current.rb
|
|
18
|
-
|
|
19
|
-
And add routes:
|
|
20
|
-
get "login", to: "sessions#new", as: :new_session
|
|
21
|
-
get "callback", to: "sessions#callback", as: :session_callback
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
|
|
5
|
-
module AhaBuilderCore
|
|
6
|
-
module Generators
|
|
7
|
-
class AuthenticationGenerator < Rails::Generators::Base
|
|
8
|
-
source_root File.expand_path("templates", __dir__)
|
|
9
|
-
|
|
10
|
-
def create_sessions_controller
|
|
11
|
-
template "sessions_controller.rb.erb", "app/controllers/sessions_controller.rb"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def create_user_model
|
|
15
|
-
template "user.rb.erb", "app/models/user.rb"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def create_current_model
|
|
19
|
-
template "current.rb.erb", "app/models/current.rb"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def create_authentication_concern
|
|
23
|
-
template "authentication.rb.erb", "app/controllers/concerns/authentication.rb"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def add_routes
|
|
27
|
-
route <<~RUBY
|
|
28
|
-
get "login", to: "sessions#new", as: :new_session
|
|
29
|
-
get "callback", to: "sessions#callback", as: :session_callback
|
|
30
|
-
RUBY
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def display_instructions
|
|
34
|
-
say "\nAuthentication setup complete!", :green
|
|
35
|
-
say "\nNext steps:"
|
|
36
|
-
say "1. Configure your environment variables:"
|
|
37
|
-
say " - APPLICATION_URL: Your application's base URL"
|
|
38
|
-
say " - AHA_AUTH_SERVER_URL: The authentication server URL"
|
|
39
|
-
say " - AHA_AUTH_CLIENT_ID: Your client ID"
|
|
40
|
-
say " - AHA_AUTH_API_KEY: Your API key (if using server-to-server operations)"
|
|
41
|
-
say "\n2. Run migrations to create the users table:"
|
|
42
|
-
say " rails generate migration CreateUsers auth_identifier:string:index email:string first_name:string last_name:string email_verified:boolean"
|
|
43
|
-
say " rails db:migrate"
|
|
44
|
-
say "\n3. Include authentication in ApplicationController:"
|
|
45
|
-
say " include Authentication"
|
|
46
|
-
say "\n4. Protect routes with:"
|
|
47
|
-
say " before_action :authenticate"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def application_name
|
|
53
|
-
Rails.application.class.module_parent.name
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
module Authentication
|
|
2
|
-
extend ActiveSupport::Concern
|
|
3
|
-
|
|
4
|
-
included do
|
|
5
|
-
before_action :authenticate
|
|
6
|
-
helper_method :current_user, :logged_in?
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def authenticate
|
|
12
|
-
if session[:session_token].present?
|
|
13
|
-
session_result = Aha::Auth.validate_session(
|
|
14
|
-
session[:session_token],
|
|
15
|
-
refresh_token: session[:refresh_token]
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
if session_result.valid?
|
|
19
|
-
# If tokens were refreshed, update stored tokens
|
|
20
|
-
if session_result.refreshed?
|
|
21
|
-
session[:session_token] = session_result.new_session_token
|
|
22
|
-
session[:refresh_token] = session_result.new_refresh_token
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Set current user
|
|
26
|
-
@current_user = User.find_by(id: session[:user_id])
|
|
27
|
-
Current.user = @current_user
|
|
28
|
-
else
|
|
29
|
-
reset_session
|
|
30
|
-
redirect_to new_session_path, alert: "Your session has expired. Please log in again."
|
|
31
|
-
end
|
|
32
|
-
else
|
|
33
|
-
redirect_to new_session_path
|
|
34
|
-
end
|
|
35
|
-
rescue Aha::Auth::ApiError => e
|
|
36
|
-
Rails.logger.error "Session validation failed: #{e.message}"
|
|
37
|
-
reset_session
|
|
38
|
-
redirect_to new_session_path, alert: "Authentication error. Please log in again."
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def current_user
|
|
42
|
-
@current_user ||= Current.user
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def logged_in?
|
|
46
|
-
current_user.present?
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
class SessionsController < ApplicationController
|
|
2
|
-
skip_before_action :authenticate, only: [:new, :callback]
|
|
3
|
-
|
|
4
|
-
def new
|
|
5
|
-
# Start authentication by redirecting to the auth URL
|
|
6
|
-
redirect_to Aha::Auth.login_url(
|
|
7
|
-
state: { return_to: params[:return_to] || root_path }.to_json,
|
|
8
|
-
redirect_uri: "#{ENV['APPLICATION_URL']}/callback"
|
|
9
|
-
), allow_other_host: true
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def callback
|
|
13
|
-
# Handle the callback from the authentication server
|
|
14
|
-
if params[:code].present?
|
|
15
|
-
result = Aha::Auth.authenticate_with_code(code: params[:code])
|
|
16
|
-
|
|
17
|
-
# Find or create a local user record linked to Builder Core user
|
|
18
|
-
user = User.find_or_initialize_by(auth_identifier: result[:user]["id"])
|
|
19
|
-
|
|
20
|
-
# Update local user attributes
|
|
21
|
-
user.update!(
|
|
22
|
-
email: result[:user]["email"],
|
|
23
|
-
first_name: result[:user]["first_name"],
|
|
24
|
-
last_name: result[:user]["last_name"],
|
|
25
|
-
email_verified: result[:user]["email_verified"]
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# Store tokens in session
|
|
29
|
-
session[:session_token] = result[:session_token]
|
|
30
|
-
session[:refresh_token] = result[:refresh_token]
|
|
31
|
-
session[:user_id] = user.id
|
|
32
|
-
|
|
33
|
-
# Parse state to get return_to path
|
|
34
|
-
state = JSON.parse(params[:state]) rescue {}
|
|
35
|
-
redirect_to state["return_to"] || root_path
|
|
36
|
-
else
|
|
37
|
-
# Authentication failed or was cancelled
|
|
38
|
-
redirect_to new_session_path, alert: "Authentication failed. Please try again."
|
|
39
|
-
end
|
|
40
|
-
rescue Aha::Auth::ApiError => e
|
|
41
|
-
Rails.logger.error "Authentication failed: #{e.message}"
|
|
42
|
-
redirect_to new_session_path, alert: "Authentication failed. Please try again."
|
|
43
|
-
end
|
|
44
|
-
end
|