authaction-ruby-sdk 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f6bf466144a51660744b7de95865c88ce7963f50a0c1a1bf023e34bed2b0df45
4
+ data.tar.gz: 50942b4e647f7761c7f5194e06fa38fb50480022bf1795419133c4cc3b8b5a95
5
+ SHA512:
6
+ metadata.gz: be653628175c59117591347d0346cd2e544fab1931712d88b97b2e5674414cf7909a747751e7bda8218dfb206df0b0f07d00e9673c962e1af8e3c86ccf20ca98
7
+ data.tar.gz: 6346a61e449bc82262fd70a136d805dfdb00b13e44bea35ea2828856113f06f73b4684d41b330c8425fb9f5f65104027652a543854f54248819f23e2fb179649
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # authaction-ruby-sdk
2
+
3
+ JWT verification SDK for Ruby backends. Validates AuthAction access tokens via JWKS — handles key fetching, caching, and rotation automatically.
4
+
5
+ Works with **Rails** (API mode or full-stack) and any **Rack** application.
6
+
7
+ ## Installation
8
+
9
+ Add to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "authaction"
13
+ ```
14
+
15
+ Then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ Or install directly:
22
+
23
+ ```bash
24
+ gem install authaction
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ### Verify a token directly
32
+
33
+ ```ruby
34
+ require "authaction"
35
+
36
+ verifier = AuthAction::JwtVerifier.new(
37
+ domain: ENV["AUTHACTION_DOMAIN"], # e.g. myapp.eu.authaction.com
38
+ audience: ENV["AUTHACTION_AUDIENCE"] # e.g. https://api.myapp.com
39
+ )
40
+
41
+ # Verify a raw token — raises TokenExpiredError / TokenInvalidError on failure
42
+ payload = verifier.verify_token(token)
43
+
44
+ # Verify from Authorization header — returns nil on missing/invalid, never raises
45
+ payload = verifier.verify_request(request.headers["Authorization"])
46
+
47
+ puts payload["sub"] # user identifier
48
+ puts payload["email"] # any JWT claim
49
+ ```
50
+
51
+ ### `verify_token`
52
+
53
+ Decodes and validates the JWT. Returns the claims hash on success.
54
+
55
+ | Condition | Raises |
56
+ |-----------|--------|
57
+ | `exp` in the past | `AuthAction::TokenExpiredError` |
58
+ | Bad signature, wrong issuer/audience, malformed JWT | `AuthAction::TokenInvalidError` |
59
+
60
+ ### `verify_request`
61
+
62
+ Convenience wrapper for HTTP handler code. Extracts the token from a `Bearer <token>` header value.
63
+
64
+ - Returns `nil` (never raises) when the header is absent, not a Bearer scheme, or the token is invalid/expired.
65
+ - Returns the claims hash on success.
66
+
67
+ ---
68
+
69
+ ## Rails Integration
70
+
71
+ ### 1. Require the concern
72
+
73
+ In `config/application.rb` or an initializer:
74
+
75
+ ```ruby
76
+ require "authaction/rails"
77
+ ```
78
+
79
+ ### 2. Include in your base controller
80
+
81
+ ```ruby
82
+ class ApplicationController < ActionController::API
83
+ include AuthAction::Rails::JwtAuthenticatable
84
+ end
85
+ ```
86
+
87
+ ### 3. Protect actions with `before_action`
88
+
89
+ ```ruby
90
+ class ProtectedController < ApplicationController
91
+ before_action :authenticate_request!
92
+
93
+ def index
94
+ render json: { sub: @current_payload["sub"] }
95
+ end
96
+ end
97
+ ```
98
+
99
+ On success, `@current_payload` (also available via the `current_payload` reader) contains the decoded JWT claims hash.
100
+
101
+ On failure the concern renders a `401 Unauthorized` JSON response automatically:
102
+
103
+ ```json
104
+ { "error": "Token has expired" }
105
+ { "error": "Missing Bearer token" }
106
+ ```
107
+
108
+ ### Public endpoints
109
+
110
+ Skip authentication for specific actions:
111
+
112
+ ```ruby
113
+ class UsersController < ApplicationController
114
+ before_action :authenticate_request!, except: [:create]
115
+
116
+ def create
117
+ # public — no token required
118
+ end
119
+
120
+ def show
121
+ render json: { sub: current_payload["sub"] }
122
+ end
123
+ end
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Configuration
129
+
130
+ The concern reads configuration from environment variables:
131
+
132
+ | Variable | Description | Example |
133
+ |----------|-------------|---------|
134
+ | `AUTHACTION_DOMAIN` | Your AuthAction tenant domain | `myapp.eu.authaction.com` |
135
+ | `AUTHACTION_AUDIENCE` | Expected `aud` claim in tokens | `https://api.myapp.com` |
136
+
137
+ ```bash
138
+ AUTHACTION_DOMAIN=your-tenant.eu.authaction.com
139
+ AUTHACTION_AUDIENCE=https://api.your-app.com
140
+ ```
141
+
142
+ When using `JwtVerifier` directly, pass these as keyword arguments instead of relying on ENV vars.
143
+
144
+ ---
145
+
146
+ ## Exceptions
147
+
148
+ ```ruby
149
+ require "authaction"
150
+
151
+ begin
152
+ payload = verifier.verify_token(token)
153
+ rescue AuthAction::TokenExpiredError
154
+ # token exp claim is in the past
155
+ rescue AuthAction::TokenInvalidError => e
156
+ # bad signature, wrong issuer/audience, malformed JWT
157
+ puts e.message
158
+ end
159
+ ```
160
+
161
+ Both error classes inherit from `AuthAction::Error < StandardError`.
162
+
163
+ ---
164
+
165
+ ## How JWKS Caching Works
166
+
167
+ `JwtVerifier` fetches public keys from `https://<domain>/.well-known/jwks.json` on first use and caches the key set in memory:
168
+
169
+ - **TTL**: 5 minutes (300 seconds). The cache is invalidated automatically after expiry.
170
+ - **Key rotation**: When a JWT arrives with an unknown `kid`, the cache is busted immediately and the JWKS endpoint is re-fetched before retrying verification.
171
+ - **Thread safety**: A `Mutex` protects the cache so the verifier is safe to share across threads (e.g. as a class-level instance variable in Rails controllers).
172
+
173
+ ---
174
+
175
+ ## Running Tests
176
+
177
+ ```bash
178
+ bundle install
179
+ bundle exec rspec
180
+ ```
181
+
182
+ Tests use [WebMock](https://github.com/bblimke/webmock) to stub the JWKS endpoint and real RSA key pairs to provide end-to-end coverage of the JWKS parsing and JWT validation path — no network calls required.
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,21 @@
1
+ require_relative "lib/authaction/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "authaction-ruby-sdk"
5
+ spec.version = AuthAction::VERSION
6
+ spec.summary = "AuthAction JWT verification for Ruby — Rails and Rack"
7
+ spec.description = "JWT verification SDK for AuthAction. Fetches and caches JWKS from the well-known endpoint, validates RS256 tokens with issuer, audience, and expiry checks. Includes a Rails concern for controller authentication."
8
+ spec.authors = ["AuthAction"]
9
+ spec.homepage = "https://github.com/authaction/authaction-ruby-sdk"
10
+ spec.license = "MIT"
11
+
12
+ spec.required_ruby_version = ">= 3.0"
13
+ spec.files = Dir["lib/**/*.rb", "*.md", "*.gemspec"]
14
+
15
+ spec.add_dependency "jwt", "~> 2.9"
16
+
17
+ spec.add_development_dependency "rspec", "~> 3.13"
18
+ spec.add_development_dependency "webmock", "~> 3.23"
19
+ spec.add_development_dependency "ostruct"
20
+ spec.add_development_dependency "rake"
21
+ end
@@ -0,0 +1,9 @@
1
+ module AuthAction
2
+ class Error < StandardError; end
3
+
4
+ # Raised when the JWT exp claim is in the past.
5
+ class TokenExpiredError < Error; end
6
+
7
+ # Raised when signature, issuer, audience, or structure is invalid.
8
+ class TokenInvalidError < Error; end
9
+ end
@@ -0,0 +1,96 @@
1
+ require "jwt"
2
+ require "net/http"
3
+ require "json"
4
+ require "uri"
5
+
6
+ module AuthAction
7
+ # Core JWT verifier.
8
+ #
9
+ # Fetches the JWKS from https://<domain>/.well-known/jwks.json, caches the
10
+ # key set in memory (TTL: 5 minutes), and busts the cache when an unknown
11
+ # kid is seen (key rotation).
12
+ #
13
+ # @example
14
+ # verifier = AuthAction::JwtVerifier.new(
15
+ # domain: "myapp.eu.authaction.com",
16
+ # audience: "https://api.myapp.com"
17
+ # )
18
+ # payload = verifier.verify_token(token)
19
+ class JwtVerifier
20
+ CACHE_TTL = 300 # seconds
21
+
22
+ def initialize(domain:, audience:)
23
+ @issuer = "https://#{domain}"
24
+ @jwks_uri = "https://#{domain}/.well-known/jwks.json"
25
+ @audience = audience
26
+ @mutex = Mutex.new
27
+ @cache = nil
28
+ @cached_at = nil
29
+ end
30
+
31
+ # Verify a raw JWT string and return the decoded payload hash.
32
+ #
33
+ # @param token [String] raw JWT
34
+ # @return [Hash] decoded claims
35
+ # @raise [TokenExpiredError] if the token is expired
36
+ # @raise [TokenInvalidError] if signature, issuer, audience, or structure is invalid
37
+ def verify_token(token)
38
+ payload, _header = JWT.decode(
39
+ token, nil, true,
40
+ algorithms: ["RS256"],
41
+ iss: @issuer,
42
+ verify_iss: true,
43
+ aud: @audience,
44
+ verify_aud: true,
45
+ jwks: method(:jwks_loader)
46
+ )
47
+ payload
48
+ rescue JWT::ExpiredSignature
49
+ raise TokenExpiredError, "Token has expired"
50
+ rescue JWT::DecodeError => e
51
+ raise TokenInvalidError, e.message
52
+ end
53
+
54
+ # Extract and verify the Bearer token from an Authorization header value.
55
+ #
56
+ # Returns nil when the header is absent or not a Bearer scheme.
57
+ # Never raises — returns nil on invalid or expired tokens.
58
+ #
59
+ # @param authorization_header [String, nil]
60
+ # @return [Hash, nil] decoded claims or nil
61
+ def verify_request(authorization_header)
62
+ return nil unless authorization_header&.start_with?("Bearer ")
63
+
64
+ token = authorization_header[7..].strip
65
+ verify_token(token)
66
+ rescue TokenExpiredError, TokenInvalidError
67
+ nil
68
+ end
69
+
70
+ private
71
+
72
+ def jwks_loader(options)
73
+ @mutex.synchronize do
74
+ @cache = nil if options[:kid_not_found]
75
+ @cache = nil if cache_expired?
76
+ @cache ||= begin
77
+ @cached_at = Time.now
78
+ fetch_jwks
79
+ end
80
+ end
81
+ end
82
+
83
+ def cache_expired?
84
+ @cached_at.nil? || (Time.now - @cached_at) >= CACHE_TTL
85
+ end
86
+
87
+ def fetch_jwks
88
+ uri = URI(@jwks_uri)
89
+ response = Net::HTTP.get_response(uri)
90
+ unless response.is_a?(Net::HTTPSuccess)
91
+ raise TokenInvalidError, "Failed to fetch JWKS: HTTP #{response.code}"
92
+ end
93
+ JSON.parse(response.body, symbolize_names: true)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,58 @@
1
+ module AuthAction
2
+ module Rails
3
+ # ActiveSupport::Concern that adds JWT authentication to a Rails controller.
4
+ #
5
+ # @example
6
+ # class ApplicationController < ActionController::API
7
+ # include AuthAction::Rails::JwtAuthenticatable
8
+ # end
9
+ #
10
+ # class ProtectedController < ApplicationController
11
+ # before_action :authenticate_request!
12
+ #
13
+ # def index
14
+ # render json: { sub: @current_payload["sub"] }
15
+ # end
16
+ # end
17
+ #
18
+ # Configure via environment variables:
19
+ # AUTHACTION_DOMAIN = myapp.eu.authaction.com
20
+ # AUTHACTION_AUDIENCE = https://api.myapp.com
21
+ module JwtAuthenticatable
22
+ extend ActiveSupport::Concern
23
+
24
+ included do
25
+ # Expose @current_payload as a helper for views (optional in API mode).
26
+ attr_reader :current_payload
27
+ end
28
+
29
+ class_methods do
30
+ # Class-level memoized verifier — one JWKS cache per controller class.
31
+ def authaction_verifier
32
+ @authaction_verifier ||= AuthAction::JwtVerifier.new(
33
+ domain: ENV.fetch("AUTHACTION_DOMAIN"),
34
+ audience: ENV.fetch("AUTHACTION_AUDIENCE")
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Verifies the Bearer JWT from the Authorization header.
42
+ # Sets @current_payload on success; renders 401 on failure.
43
+ def authenticate_request!
44
+ header = request.headers["Authorization"]
45
+ unless header&.start_with?("Bearer ")
46
+ render json: { error: "Missing Bearer token" }, status: :unauthorized and return
47
+ end
48
+
49
+ token = header[7..].strip
50
+ @current_payload = self.class.authaction_verifier.verify_token(token)
51
+ rescue AuthAction::TokenExpiredError
52
+ render json: { error: "Token has expired" }, status: :unauthorized
53
+ rescue AuthAction::TokenInvalidError => e
54
+ render json: { error: e.message }, status: :unauthorized
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1 @@
1
+ require "authaction/rails/jwt_authenticatable"
@@ -0,0 +1,3 @@
1
+ module AuthAction
2
+ VERSION = "0.1.0"
3
+ end
data/lib/authaction.rb ADDED
@@ -0,0 +1,20 @@
1
+ require "authaction/version"
2
+ require "authaction/errors"
3
+ require "authaction/jwt_verifier"
4
+
5
+ # AuthAction JWT verification SDK for Ruby.
6
+ #
7
+ # @example Basic usage
8
+ # verifier = AuthAction::JwtVerifier.new(
9
+ # domain: "myapp.eu.authaction.com",
10
+ # audience: "https://api.myapp.com"
11
+ # )
12
+ # payload = verifier.verify_token(token)
13
+ #
14
+ # @example Rails controller (include the concern)
15
+ # require "authaction/rails"
16
+ # class ApplicationController < ActionController::API
17
+ # include AuthAction::Rails::JwtAuthenticatable
18
+ # end
19
+ module AuthAction
20
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: authaction-ruby-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - AuthAction
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.23'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.23'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ostruct
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: JWT verification SDK for AuthAction. Fetches and caches JWKS from the
84
+ well-known endpoint, validates RS256 tokens with issuer, audience, and expiry checks.
85
+ Includes a Rails concern for controller authentication.
86
+ email:
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - README.md
92
+ - authaction.gemspec
93
+ - lib/authaction.rb
94
+ - lib/authaction/errors.rb
95
+ - lib/authaction/jwt_verifier.rb
96
+ - lib/authaction/rails.rb
97
+ - lib/authaction/rails/jwt_authenticatable.rb
98
+ - lib/authaction/version.rb
99
+ homepage: https://github.com/authaction/authaction-ruby-sdk
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '3.0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.5.22
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: AuthAction JWT verification for Ruby — Rails and Rack
122
+ test_files: []