omniauth-openai 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: c4510db682d5cc881cd21ff66a13b91f19c59e70f2c34e23c38e24b9b7d954d4
4
+ data.tar.gz: 8e4c7ae32887385fc221e700af78787e87b017369eb5de1acb1f0e5efa5c2806
5
+ SHA512:
6
+ metadata.gz: 73c540a83c85e9fa03f71d1be70d1ea2c811273cc1d9c8e744b66b60a3d81e9b3de17649509a998042e5de63dae8656c3828d2a68143695d635e4a6dc0ca9cbc
7
+ data.tar.gz: 73a11709ac1f2366346b311a2ce2c8c6e26a9cbfd6eb5d2e837bc7cbb871522c0d59d3b3ac7346ab84319e3e1a3a23a4a0e33534d85a605b38bf0e9a4baf6540
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-01
4
+
5
+ First release: Sign in with ChatGPT for OmniAuth and Devise, with PKCE, a
6
+ nonce, and full ID token verification against OpenAI's published keys.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ajaynomics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # omniauth-openai
2
+
3
+ An [OmniAuth](https://github.com/omniauth/omniauth) strategy for
4
+ [Sign in with ChatGPT](https://developers.openai.com/siwc/website), OpenAI's
5
+ OpenID Connect sign-in.
6
+
7
+ It runs the authorization code flow with PKCE and a nonce, then verifies the
8
+ ID token's signature, issuer, audience, expiry, and nonce against OpenAI's
9
+ published keys before your application sees an identity. It makes no userinfo
10
+ call and hands your application no OpenAI tokens: sign-in proves who someone
11
+ is, nothing more.
12
+
13
+ Unofficial. Not affiliated with or endorsed by OpenAI.
14
+
15
+ ## Before you start
16
+
17
+ Sign in with ChatGPT is in limited access. Request it through
18
+ [OpenAI's interest form](https://openai.com/form/sign-in-with-chatgpt-interest/).
19
+ OpenAI then issues a client ID (`oaiapp_…`), and a client secret if you ask for
20
+ a confidential client, and registers your exact callback URL, for example
21
+ `https://example.com/users/auth/openai/callback`.
22
+
23
+ ## Install
24
+
25
+ ```ruby
26
+ gem "omniauth-openai"
27
+ gem "omniauth-rails_csrf_protection" # Rails: sign-in must start with a POST
28
+ ```
29
+
30
+ ## Use
31
+
32
+ ### Devise
33
+
34
+ ```ruby
35
+ # config/initializers/devise.rb
36
+ config.omniauth :openai, ENV["OPENAI_CLIENT_ID"], ENV["OPENAI_CLIENT_SECRET"]
37
+ ```
38
+
39
+ ```ruby
40
+ # app/models/user.rb
41
+ devise :omniauthable, omniauth_providers: %i[openai]
42
+ ```
43
+
44
+ ```erb
45
+ <%= button_to "Continue with ChatGPT", user_openai_omniauth_authorize_path, data: { turbo: false } %>
46
+ ```
47
+
48
+ ### Plain Rack or Rails without Devise
49
+
50
+ ```ruby
51
+ use OmniAuth::Builder do
52
+ provider :openai, ENV["OPENAI_CLIENT_ID"], ENV["OPENAI_CLIENT_SECRET"]
53
+ end
54
+ ```
55
+
56
+ A public client has no secret: pass `nil` and the strategy sends only its
57
+ client ID.
58
+
59
+ ### The auth hash
60
+
61
+ ```ruby
62
+ {
63
+ "provider" => "openai",
64
+ "uid" => "user-…", # the ID token's sub: key accounts on this
65
+ "info" => {
66
+ "email" => "person@example.com", # present only when email_verified is true
67
+ "name" => "Chat Person",
68
+ "image" => "https://…"
69
+ },
70
+ "credentials" => {},
71
+ "extra" => { "raw_info" => { … all verified ID token claims … } }
72
+ }
73
+ ```
74
+
75
+ Key accounts on `provider` and `uid`, not email: one email can belong to more
76
+ than one OpenAI account. If you link to an existing account by email, link only
77
+ to one whose email your application has itself confirmed, or an attacker who
78
+ registers the address first gains the real owner's sign-in.
79
+
80
+ ## Options
81
+
82
+ | Option | Default | Meaning |
83
+ | --- | --- | --- |
84
+ | `scope` | `"openid profile email"` | Requested scopes. |
85
+ | `client_auth_method` | `:basic` with a secret, `:none` without | `:basic` (client_secret_basic), `:post` (client_secret_post), or `:none`. |
86
+ | `redirect_uri` | the callback URL | Set it when a proxy changes the host the app sees. Must match the registered URL exactly. |
87
+ | `leeway` | `5` | Seconds of clock skew allowed on `exp` and `iat`. |
88
+ | `issuer`, `jwks_uri`, `client_options` | OpenAI's | Endpoints from `https://auth.openai.com/.well-known/openid-configuration`. |
89
+
90
+ Signing keys are cached per process for an hour. A token signed with a key the
91
+ cache lacks triggers one fresh download, at most once every five minutes.
92
+
93
+ ## Failures
94
+
95
+ Every verification failure ends in OmniAuth's failure path with
96
+ `:invalid_credentials`, so a bad token never reaches your callback. A forged or
97
+ missing `state` fails as `:csrf_detected`; a person who declines consent fails
98
+ as `:access_denied`.
99
+
100
+ ## Development
101
+
102
+ ```sh
103
+ bundle install
104
+ bundle exec rake test
105
+ JWT_VERSION="~> 2.10" bundle update jwt && bundle exec rake test
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module OpenAI
5
+ # The issuer's signing keys, shared by every request in the process.
6
+ #
7
+ # Keys are kept for +ttl+ seconds. When a token names a key the set lacks,
8
+ # ruby-jwt asks again with +kid_not_found+, and the set downloads afresh so
9
+ # a rotated key is picked up at once. Those forced downloads happen at most
10
+ # once per +refresh_interval+, so a stream of bad tokens cannot turn into a
11
+ # stream of requests to the issuer.
12
+ class KeySet
13
+ def initialize(ttl: 3600, refresh_interval: 300, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
14
+ @ttl = ttl
15
+ @refresh_interval = refresh_interval
16
+ @clock = clock
17
+ @lock = Mutex.new
18
+ @keys = nil
19
+ @fetched_at = nil
20
+ end
21
+
22
+ # Returns the JWKS hash, calling the block to download it when needed.
23
+ def fetch(kid_not_found: false)
24
+ @lock.synchronize do
25
+ if stale? || (kid_not_found && refreshable?)
26
+ @keys = yield
27
+ @fetched_at = now
28
+ end
29
+ @keys
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def stale?
36
+ @keys.nil? || now - @fetched_at >= @ttl
37
+ end
38
+
39
+ def refreshable?
40
+ now - @fetched_at >= @refresh_interval
41
+ end
42
+
43
+ def now
44
+ @clock.call
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module OpenAI
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth/openai/version"
4
+ require "omniauth/strategies/openai"
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "jwt"
5
+ require "omniauth-oauth2"
6
+ require "omniauth/openai/key_set"
7
+ require "rack/utils"
8
+ require "securerandom"
9
+ require "uri"
10
+
11
+ module OmniAuth
12
+ module Strategies
13
+ # Sign in with ChatGPT: OpenID Connect over the authorization code flow,
14
+ # with PKCE and a nonce. See https://developers.openai.com/siwc/website.
15
+ #
16
+ # The strategy establishes who the person is and nothing more. It trusts
17
+ # only the ID token, after checking its signature, issuer, audience,
18
+ # expiry, and nonce. It makes no userinfo call and returns no OpenAI
19
+ # tokens, because identity sign-in needs none.
20
+ #
21
+ # The endpoints come from https://auth.openai.com/.well-known/openid-configuration.
22
+ # They are fixed options rather than discovered at boot, so a slow
23
+ # discovery request cannot hold up an application starting.
24
+ class OpenAI < OmniAuth::Strategies::OAuth2
25
+ ISSUER = "https://auth.openai.com"
26
+ KEY_SETS = Hash.new { |sets, uri| sets[uri] = OmniAuth::OpenAI::KeySet.new }
27
+ KEY_SETS_LOCK = Mutex.new
28
+
29
+ option :name, "openai"
30
+ option :scope, "openid profile email"
31
+ option :pkce, true
32
+ option :issuer, ISSUER
33
+ option :jwks_uri, "#{ISSUER}/.well-known/jwks.json"
34
+ # :basic (client_secret_basic), :post (client_secret_post), or :none for
35
+ # a public client, the three methods OpenAI's discovery document lists.
36
+ # Left nil, a client with a secret uses :basic, the OpenID Connect
37
+ # default, and one without uses :none.
38
+ option :client_auth_method, nil
39
+ option :leeway, 5
40
+ option :client_options, {
41
+ site: ISSUER,
42
+ authorize_url: "/api/accounts/authorize",
43
+ token_url: "/api/accounts/oauth/token"
44
+ }
45
+
46
+ uid { id_token_claims.fetch("sub") }
47
+
48
+ # Applications commonly match accounts by email, so the email is
49
+ # included only when OpenAI says the person verified it.
50
+ info do
51
+ {
52
+ email: (id_token_claims["email"] if id_token_claims["email_verified"] == true),
53
+ name: id_token_claims["name"],
54
+ first_name: id_token_claims["given_name"],
55
+ last_name: id_token_claims["family_name"],
56
+ nickname: id_token_claims["preferred_username"],
57
+ image: id_token_claims["picture"]
58
+ }.compact
59
+ end
60
+
61
+ extra do
62
+ { "raw_info" => id_token_claims }
63
+ end
64
+
65
+ # OmniAuth merges credentials blocks down the class chain, so the
66
+ # parent's token keys can only be dropped by overriding the method.
67
+ def credentials
68
+ {}
69
+ end
70
+
71
+ # The token request carries client_id in the body for every method.
72
+ # oauth2's :tls_client_auth scheme does exactly that and nothing more;
73
+ # the secret, if any, is added by #token_params.
74
+ def client
75
+ ::OAuth2::Client.new(options.client_id, options.client_secret,
76
+ deep_symbolize(options.client_options).merge(auth_scheme: :tls_client_auth))
77
+ end
78
+
79
+ # OAuth2 appends the callback's query string, but OpenAI compares
80
+ # redirect_uri exactly with the one sent to the authorize endpoint.
81
+ def callback_url
82
+ options.redirect_uri || (full_host + callback_path)
83
+ end
84
+
85
+ def authorize_params
86
+ super.tap do |params|
87
+ params[:nonce] = session[nonce_session_key] = SecureRandom.urlsafe_base64(32)
88
+ end
89
+ end
90
+
91
+ def token_params
92
+ params = super
93
+ case client_auth_method
94
+ when :basic then params.merge(headers: { "Authorization" => basic_authorization })
95
+ when :post then params.merge(client_secret: options.client_secret)
96
+ else params
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ attr_reader :id_token_claims
103
+
104
+ # Runs inside OAuth2#callback_phase, which turns a CallbackError into an
105
+ # :invalid_credentials failure. A token that fails verification never
106
+ # reaches uid, info, or the application.
107
+ def build_access_token
108
+ expected_nonce = session.delete(nonce_session_key)
109
+ super.tap do |token|
110
+ @id_token_claims = verify_id_token(token.response.parsed["id_token"], expected_nonce)
111
+ end
112
+ end
113
+
114
+ def verify_id_token(id_token, expected_nonce)
115
+ raise CallbackError.new(:invalid_id_token, "The token response carried no ID token") if id_token.to_s.empty?
116
+
117
+ claims, = ::JWT.decode(
118
+ id_token, nil, true,
119
+ algorithms: [ "RS256" ],
120
+ jwks: ->(loader_options) { signing_keys(**loader_options.slice(:kid_not_found)) },
121
+ iss: options.issuer, verify_iss: true,
122
+ aud: options.client_id, verify_aud: true,
123
+ required_claims: %w[sub exp iat],
124
+ verify_iat: true,
125
+ leeway: options.leeway
126
+ )
127
+
128
+ verify_authorized_party(claims)
129
+ verify_nonce(claims, expected_nonce)
130
+ claims
131
+ rescue ::JWT::DecodeError, ::OAuth2::Error, ::Faraday::Error, ::JSON::ParserError => e
132
+ raise CallbackError.new(:invalid_id_token, e.message)
133
+ end
134
+
135
+ # OpenID Connect Core 3.1.3.7: a token for several audiences must name
136
+ # this client as the party it was issued to.
137
+ def verify_authorized_party(claims)
138
+ audiences = Array(claims["aud"])
139
+ return if audiences.one? || claims["azp"] == options.client_id
140
+
141
+ raise CallbackError.new(:invalid_id_token, "The ID token was issued to another party")
142
+ end
143
+
144
+ def verify_nonce(claims, expected_nonce)
145
+ return if expected_nonce && Rack::Utils.secure_compare(claims["nonce"].to_s, expected_nonce)
146
+
147
+ raise CallbackError.new(:invalid_id_token, "The ID token nonce did not match")
148
+ end
149
+
150
+ def signing_keys(kid_not_found: false)
151
+ key_set.fetch(kid_not_found: kid_not_found) do
152
+ client.request(:get, options.jwks_uri).parsed
153
+ end
154
+ end
155
+
156
+ def key_set
157
+ KEY_SETS_LOCK.synchronize { KEY_SETS[options.jwks_uri] }
158
+ end
159
+
160
+ def client_auth_method
161
+ method = options.client_auth_method&.to_sym
162
+ method || (options.client_secret.to_s.empty? ? :none : :basic)
163
+ end
164
+
165
+ # RFC 6749 2.3.1: both halves are form-encoded before Base64.
166
+ def basic_authorization
167
+ pair = [ options.client_id, options.client_secret ].map { |part| URI.encode_www_form_component(part.to_s) }.join(":")
168
+ "Basic #{[ pair ].pack("m0")}"
169
+ end
170
+
171
+ def nonce_session_key
172
+ "omniauth.#{name}.nonce"
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ OmniAuth.config.add_camelization "openai", "OpenAI"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth/openai"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/omniauth/openai/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "omniauth-openai"
7
+ spec.version = OmniAuth::OpenAI::VERSION
8
+ spec.authors = [ "Ajay Krishnan" ]
9
+ spec.email = [ "omniauth-openai@ajay.to" ]
10
+
11
+ spec.summary = "OmniAuth strategy for Sign in with ChatGPT (OpenAI OpenID Connect)."
12
+ spec.description = <<~DESC
13
+ Adds "Sign in with ChatGPT" to Rack and Rails applications through
14
+ OmniAuth and Devise. Authorization code flow with PKCE and a nonce; the
15
+ ID token's signature, issuer, audience, expiry, and nonce are verified
16
+ against OpenAI's published keys before the application sees an identity.
17
+ Unofficial; not affiliated with or endorsed by OpenAI.
18
+ DESC
19
+ spec.homepage = "https://github.com/ajaynomics/omniauth-openai"
20
+ spec.license = "MIT"
21
+ spec.required_ruby_version = ">= 3.2.0"
22
+
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
25
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
26
+ spec.metadata["rubygems_mfa_required"] = "true"
27
+
28
+ spec.files = Dir.glob("lib/**/*.rb") + %w[README.md LICENSE CHANGELOG.md omniauth-openai.gemspec]
29
+ spec.require_paths = [ "lib" ]
30
+
31
+ spec.add_dependency "jwt", ">= 2.7", "< 4"
32
+ spec.add_dependency "omniauth-oauth2", "~> 1.8"
33
+
34
+ spec.add_development_dependency "minitest", ">= 5.20"
35
+ spec.add_development_dependency "rack-test", "~> 2.1"
36
+ spec.add_development_dependency "rake", "~> 13.0"
37
+ spec.add_development_dependency "webmock", "~> 3.20"
38
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-openai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ajay Krishnan
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: jwt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.7'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: omniauth-oauth2
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.8'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.8'
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '5.20'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '5.20'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rack-test
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.1'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.1'
74
+ - !ruby/object:Gem::Dependency
75
+ name: rake
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '13.0'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '13.0'
88
+ - !ruby/object:Gem::Dependency
89
+ name: webmock
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '3.20'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '3.20'
102
+ description: |
103
+ Adds "Sign in with ChatGPT" to Rack and Rails applications through
104
+ OmniAuth and Devise. Authorization code flow with PKCE and a nonce; the
105
+ ID token's signature, issuer, audience, expiry, and nonce are verified
106
+ against OpenAI's published keys before the application sees an identity.
107
+ Unofficial; not affiliated with or endorsed by OpenAI.
108
+ email:
109
+ - omniauth-openai@ajay.to
110
+ executables: []
111
+ extensions: []
112
+ extra_rdoc_files: []
113
+ files:
114
+ - CHANGELOG.md
115
+ - LICENSE
116
+ - README.md
117
+ - lib/omniauth-openai.rb
118
+ - lib/omniauth/openai.rb
119
+ - lib/omniauth/openai/key_set.rb
120
+ - lib/omniauth/openai/version.rb
121
+ - lib/omniauth/strategies/openai.rb
122
+ - omniauth-openai.gemspec
123
+ homepage: https://github.com/ajaynomics/omniauth-openai
124
+ licenses:
125
+ - MIT
126
+ metadata:
127
+ source_code_uri: https://github.com/ajaynomics/omniauth-openai
128
+ changelog_uri: https://github.com/ajaynomics/omniauth-openai/blob/main/CHANGELOG.md
129
+ bug_tracker_uri: https://github.com/ajaynomics/omniauth-openai/issues
130
+ rubygems_mfa_required: 'true'
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 3.2.0
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 4.0.16
146
+ specification_version: 4
147
+ summary: OmniAuth strategy for Sign in with ChatGPT (OpenAI OpenID Connect).
148
+ test_files: []