omniauth-carta 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: 1102ede87797f5363ec6d747c970aa3f10d7f5d05df1677cc0e16119ee404408
4
+ data.tar.gz: 935505ac178ca89e9076886c496465169f029439028d2b78e958e2585d3a8b35
5
+ SHA512:
6
+ metadata.gz: da2ffe66956d0da878acd243a31bc76462e01acaf2a16a02b7ea8f0c073fdfc2237d17c1f3107824687a69b14ef8b7df29f95cbbca5976ce453a0d310d4a956f
7
+ data.tar.gz: 9835272275b508ba9f460e0f1dc43c97d5840868ad9bc91355fc568113246d07ac3e03072539d94430d5095b3f2ae6e9c684b501d936f75cec7584e97354f45c
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-09
4
+
5
+ - Add the Carta OmniAuth OAuth2 strategy with HTTP Basic token exchange.
6
+ - Fetch UserInfo for stable subject IDs and optional profile/email fields.
7
+ - Enable S256 PKCE and preserve OmniAuth state and request CSRF protection.
8
+ - Support custom endpoints, scopes, and callback URLs.
9
+ - Add Minitest fixtures, middleware integration tests, and Ruby CI.
10
+ - Preserve Carta-returned scopes in the auth hash credentials.
11
+ - Preserve refresh tokens even when the token response omits expiry information.
12
+ - Verify playground endpoint overrides with API scopes and document the working Rails configuration.
13
+ - Document scope reauthorization, resource permissions, and the application-owned API/refresh boundary.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avi Flombaum
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,196 @@
1
+ # OmniAuth Carta
2
+
3
+ An [OmniAuth OAuth2](https://github.com/omniauth/omniauth-oauth2) strategy for authenticating Carta users in Ruby and Rails applications. Requires Ruby 3.2+ and OmniAuth 2 (provided by the OAuth2 dependency).
4
+
5
+ The strategy exchanges authorization codes, fetches Carta's UserInfo, and returns the standard OmniAuth auth hash. It enables S256 PKCE and state verification by default.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "omniauth-carta", "~> 0.1.0"
13
+ ```
14
+
15
+ Run `bundle install`. Source code and issues are hosted on [GitHub](https://github.com/aviflombaum/omniauth-carta).
16
+
17
+ ## Register with Carta
18
+
19
+ Register an authorization-code application in the [Carta Developer Portal](https://developers.app.carta.com/). Enable `openid`, `profile`, and `email` for the default configuration. Register your exact callback URL, such as `https://your-app.example/auth/carta/callback`, and obtain the client ID and secret. Keep secrets in your application's credentials or environment.
20
+
21
+ See Carta's [authorization guide](https://docs.carta.com/api-platform/guides/guides/authorization/using-the-authorization-code-flow/) and [OIDC guide](https://docs.carta.com/api-platform/guides/references/oidc/).
22
+
23
+ ## Rails
24
+
25
+ Add Rails' OmniAuth request CSRF integration:
26
+
27
+ ```ruby
28
+ # Gemfile
29
+ gem "omniauth-rails_csrf_protection", "~> 1.0"
30
+ ```
31
+
32
+ Configure the middleware:
33
+
34
+ ```ruby
35
+ # config/initializers/omniauth.rb
36
+ Rails.application.config.middleware.use OmniAuth::Builder do
37
+ provider :carta, Rails.application.credentials.dig(:carta, :client_id),
38
+ Rails.application.credentials.dig(:carta, :client_secret)
39
+ end
40
+ ```
41
+
42
+ Start authentication with a POST form. Keep Turbo from treating the external redirect as a fetch:
43
+
44
+ ```erb
45
+ <%= button_to "Connect Carta", "/auth/carta", method: :post, data: { turbo: false } %>
46
+ ```
47
+
48
+ Route the callback and failure endpoint to your application:
49
+
50
+ ```ruby
51
+ # config/routes.rb
52
+ get "/auth/carta/callback", to: "carta_sessions#create"
53
+ get "/auth/failure", to: "carta_sessions#failure"
54
+ ```
55
+
56
+ In `CartaSessionsController#create`, read `request.env.fetch("omniauth.auth")`. Find or create your application account using the provider and UID together; perform account linking according to your application's policy. Store the application's user ID in the session and any Carta tokens in encrypted server-side storage. The failure action should display a generic retry message, without rendering raw provider error details. API-only Rails apps also need cookie/session middleware for OmniAuth.
57
+
58
+ ## Plain Ruby / Rack
59
+
60
+ Require `omniauth-carta`, install session middleware before `OmniAuth::Builder`, and register `provider :carta, client_id, client_secret`. Render a POST form with a token from `Rack::Protection::AuthenticityToken.token(env["rack.session"])`. OmniAuth's default request validation checks that token; leave it enabled.
61
+
62
+ The runnable [Rack example](examples/rack/config.ru) demonstrates the complete form, callback, session, and failure flow:
63
+
64
+ ```sh
65
+ cd examples/rack
66
+ bundle install
67
+ export CARTA_CLIENT_ID=your-client-id
68
+ export CARTA_CLIENT_SECRET=your-client-secret
69
+ export SESSION_SECRET=$(ruby -rsecurerandom -e 'puts SecureRandom.hex(64)')
70
+ bundle exec rackup -p 9292
71
+ ```
72
+
73
+ Register `http://localhost:9292/auth/carta/callback` with your Carta development application and open `http://localhost:9292`. The example displays the UID but does not persist accounts or tokens. Use HTTPS and secure cookies in production.
74
+
75
+ ## Options
76
+
77
+ | Option | Default | Purpose |
78
+ | --- | --- | --- |
79
+ | `scope` | `"openid profile email"` | Space-delimited requested scopes |
80
+ | `pkce` | `true` | S256 proof key for code exchange |
81
+ | `userinfo_url` | `"/o/userinfo/"` | Relative to client site, or an absolute URL |
82
+ | `callback_url` | Generated from host and callback path | Explicit registered redirect URI |
83
+ | `client_options[:site]` | `"https://login.app.carta.com"` | OAuth host |
84
+ | `client_options[:authorize_url]` | `"/o/authorize/"` | Authorization endpoint |
85
+ | `client_options[:token_url]` | `"/o/access_token/"` | Token endpoint |
86
+ | `client_options[:auth_scheme]` | `:basic_auth` | Client authentication method |
87
+
88
+ Standard OmniAuth options such as `name`, `path_prefix`, `callback_path`, `authorize_params`, and `setup` also apply. Generated callback URLs omit request query parameters so the token exchange uses the same redirect URI. Use an explicit `callback_url` if the registered URI includes a query string. Configure your application's trusted proxy/host settings correctly when generating URLs behind a proxy.
89
+
90
+ Add only the API scopes your application needs, keeping `openid` for identity lookup:
91
+
92
+ ```ruby
93
+ provider :carta, ENV.fetch("CARTA_CLIENT_ID"), ENV.fetch("CARTA_CLIENT_SECRET"),
94
+ scope: "openid profile email read_issuer_info"
95
+ ```
96
+
97
+ Scope values must be enabled for your registered Carta application. `profile` and `email` are optional; `openid` is required to use UserInfo. API permissions remain subject to the consenting user's Carta roles. See [Carta's scope reference](https://docs.carta.com/api-platform/guides/references/scopes/).
98
+
99
+ ### Playground
100
+
101
+ New Carta applications use playground credentials until production promotion and approval. This strategy defaults to production, so configure playground endpoints explicitly. The following endpoint configuration was verified with a Rails integration; confirm it against your Developer Portal's Playground tab:
102
+
103
+ ```ruby
104
+ provider :carta, ENV.fetch("CARTA_CLIENT_ID"), ENV.fetch("CARTA_CLIENT_SECRET"),
105
+ client_options: {
106
+ site: "https://login.playground.carta.team",
107
+ authorize_url: "https://login.playground.carta.team/o/authorize/",
108
+ token_url: "https://login.playground.carta.team/o/access_token/"
109
+ },
110
+ userinfo_url: "https://login.playground.carta.team/o/userinfo/"
111
+ ```
112
+
113
+ The same `/auth/carta/callback` path works in either environment. Its full URL must exactly match the callback registered with the corresponding Carta application, including scheme, hostname, path, and any trailing slash. The client secret is used during token exchange, so an authorization-page rejection can indicate that playground credentials were sent to production or that the registered redirect URI does not match.
114
+
115
+ Use trusted HTTPS endpoint configuration. See [Carta's playground guide](https://docs.carta.com/api-platform/guides/getting-started/playground-environment/).
116
+
117
+ ## Auth hash
118
+
119
+ An illustrative successful callback provides:
120
+
121
+ ```ruby
122
+ {
123
+ "provider" => "carta",
124
+ "uid" => "carta-user-123", # UserInfo sub; stable identity, not email
125
+ "info" => {
126
+ "name" => "Alex Example",
127
+ "first_name" => "Alex",
128
+ "last_name" => "Example",
129
+ "email" => "alex@example.com"
130
+ },
131
+ "credentials" => {
132
+ "token" => "ACCESS_TOKEN",
133
+ "refresh_token" => "REFRESH_TOKEN",
134
+ "expires_at" => 1_800_000_000,
135
+ "expires" => true,
136
+ "scope" => "openid profile email"
137
+ },
138
+ "extra" => {
139
+ "raw_info" => {
140
+ "sub" => "carta-user-123",
141
+ "name" => "Alex Example",
142
+ "given_name" => "Alex",
143
+ "family_name" => "Example",
144
+ "email" => "alex@example.com"
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ Profile fields depend on granted scopes and available claims; missing fields are omitted. UserInfo is fetched once per authentication. A missing or invalid `sub` fails authentication. Credentials follow `omniauth-oauth2` conventions. A returned refresh token is preserved even when expiry information is absent. `scope` contains the space-delimited scopes returned in Carta's token response when present; the strategy does not substitute configured/requested scopes when the response omits it. An omitted scope field is not evidence that access was denied. Record requested scopes separately if you need them for diagnostics.
151
+
152
+ Identity comes from an authenticated HTTPS UserInfo request. The strategy does not decode, expose, or validate ID tokens and is not a full OIDC relying-party implementation. Do not treat an unverified ID token or an email address as proof of account ownership.
153
+
154
+ ## Using Carta API permissions
155
+
156
+ Identity scopes (`openid profile email`) allow sign-in; they do not authorize portfolio imports. For example, browsing investor companies requires:
157
+
158
+ ```ruby
159
+ scope: "openid profile email read_investor_firms read_investor_funds read_investor_investments"
160
+ ```
161
+
162
+ Add the read scopes needed for cap tables, securities, or valuations to your application's request. Enabling a scope in the Carta portal does not add it to an existing access token: reconnect the user after expanding the requested scopes. A successful OAuth callback does not guarantee access to every API resource; resource access also depends on the consenting user's permissions and selected resources. Handle API authorization failures in your application.
163
+
164
+ Keep Carta API clients, pagination, fund/company mappings, import persistence, and refresh coordination in the consuming application. This gem's contract is the OAuth strategy and auth hash. The Rails integration verified investor firms/funds/investments, shareholder portfolios, and managed issuers through separate application-level API calls; those resources are not part of the gem.
165
+
166
+ ## Refreshing API access
167
+
168
+ OmniAuth handles login; your application manages subsequent API calls and token refresh. Reuse the OAuth2 client with the same endpoint configuration:
169
+
170
+ ```ruby
171
+ strategy = OmniAuth::Strategies::Carta.new(nil,
172
+ ENV.fetch("CARTA_CLIENT_ID"), ENV.fetch("CARTA_CLIENT_SECRET"))
173
+ token = OAuth2::AccessToken.new(strategy.client, stored_access_token,
174
+ refresh_token: stored_refresh_token, expires_at: stored_expires_at)
175
+ renewed = token.refresh!
176
+ # Atomically persist renewed.token, renewed.refresh_token, and renewed.expires_at.
177
+ # Preserve the stored refresh token if a token response omits its replacement.
178
+ ```
179
+
180
+ Use your playground options when constructing that client for development. Carta rotates refresh tokens; serialize refreshes for a given connection and persist the new pair together. If refresh is rejected or expired, ask the user to authorize again. Do not put access/refresh tokens in cookies or browser storage. See the [refresh flow](https://docs.carta.com/api-platform/guides/guides/authorization/using-the-authorization-code-flow/#refresh-token-flow).
181
+
182
+ ## Development
183
+
184
+ ```sh
185
+ bundle install
186
+ bundle exec rake # Minitest + WebMock; no real network calls
187
+ bundle exec rake build # pkg/omniauth-carta-0.1.0.gem
188
+ ```
189
+
190
+ Tests use synthetic [JSON fixtures](test/fixtures), not captured credentials. They exercise the real middleware, token HTTP requests, PKCE/state, CSRF protection, identity mapping, endpoint overrides, errors, and token rotation. CI is configured for Ruby 3.2, 3.3, 3.4, and 4.0. See [implementation plans](plans/README.md) for decisions and verification results.
191
+
192
+ Live playground OAuth and subsequent API access were verified in a consuming Rails application on 2026-09-09. Production sign-in has not been verified. Automated tests use synthetic fixtures; they do not establish that a particular Carta application is provisioned correctly.
193
+
194
+ ## License
195
+
196
+ [MIT](LICENSE.txt). This is an independent integration, not an official Carta SDK.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Carta
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth-oauth2"
4
+
5
+ module OmniAuth
6
+ module Strategies
7
+ class Carta < OmniAuth::Strategies::OAuth2
8
+ option :name, "carta"
9
+ option :scope, "openid profile email"
10
+ option :pkce, true
11
+ option :userinfo_url, "/o/userinfo/"
12
+ option :client_options,
13
+ site: "https://login.app.carta.com",
14
+ authorize_url: "/o/authorize/",
15
+ token_url: "/o/access_token/",
16
+ auth_scheme: :basic_auth
17
+
18
+ uid { raw_info.fetch("sub") }
19
+
20
+ info do
21
+ {
22
+ name: raw_info["name"],
23
+ first_name: raw_info["given_name"],
24
+ last_name: raw_info["family_name"],
25
+ email: raw_info["email"]
26
+ }.compact
27
+ end
28
+
29
+ # Extend the inherited OAuth2 credentials without changing token/expiry fields.
30
+ credentials do
31
+ hash = {}
32
+ hash["refresh_token"] = access_token.refresh_token if access_token.refresh_token
33
+ scope = access_token.params["scope"]
34
+ hash["scope"] = scope if scope.is_a?(String)
35
+ hash
36
+ end
37
+
38
+ extra do
39
+ { raw_info: raw_info }
40
+ end
41
+
42
+ def raw_info
43
+ @raw_info ||= begin
44
+ response = access_token.get(options.userinfo_url, headers: { "Accept" => "application/json" }).parsed
45
+ unless response.is_a?(Hash) && response["sub"].is_a?(String) && !response["sub"].strip.empty?
46
+ raise CallbackError.new(:invalid_userinfo, "Carta UserInfo must contain a non-empty string sub")
47
+ end
48
+ response
49
+ end
50
+ end
51
+
52
+ def callback_url
53
+ options[:callback_url] || (full_host + callback_path)
54
+ end
55
+
56
+ protected
57
+
58
+ # omniauth-oauth2 1.9 assumes a stored state exists, including on replay.
59
+ def secure_compare(left, right)
60
+ return false unless left.is_a?(String) && right.is_a?(String)
61
+
62
+ super
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth/carta/version"
4
+ require "omniauth/strategies/carta"
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-carta
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Avi Flombaum
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: omniauth-oauth2
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.9'
26
+ description: An OmniAuth OAuth2 strategy for Carta using its UserInfo endpoint, with
27
+ PKCE support.
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - CHANGELOG.md
33
+ - LICENSE.txt
34
+ - README.md
35
+ - lib/omniauth-carta.rb
36
+ - lib/omniauth/carta/version.rb
37
+ - lib/omniauth/strategies/carta.rb
38
+ homepage: https://github.com/aviflombaum/omniauth-carta
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ rubygems_mfa_required: 'true'
43
+ source_code_uri: https://github.com/aviflombaum/omniauth-carta
44
+ changelog_uri: https://github.com/aviflombaum/omniauth-carta/blob/main/CHANGELOG.md
45
+ bug_tracker_uri: https://github.com/aviflombaum/omniauth-carta/issues
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 4.0.14
61
+ specification_version: 4
62
+ summary: Carta OAuth2 authentication for OmniAuth
63
+ test_files: []