standard_id-oidc 0.6.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +231 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/standard_id/oidc/application.css +15 -0
  6. data/app/controllers/standard_id/oidc/application_controller.rb +45 -0
  7. data/app/controllers/standard_id/oidc/consent_controller.rb +91 -0
  8. data/app/controllers/standard_id/oidc/discovery_controller.rb +106 -0
  9. data/app/controllers/standard_id/oidc/revocation_controller.rb +49 -0
  10. data/app/helpers/standard_id/oidc/application_helper.rb +6 -0
  11. data/app/jobs/standard_id/oidc/application_job.rb +6 -0
  12. data/app/mailers/standard_id/oidc/application_mailer.rb +8 -0
  13. data/app/models/standard_id/oidc/application_record.rb +7 -0
  14. data/app/models/standard_id/oidc/consent_grant.rb +41 -0
  15. data/app/models/standard_id/oidc/revoked_token.rb +31 -0
  16. data/app/views/layouts/standard_id/oidc/application.html.erb +17 -0
  17. data/config/routes.rb +15 -0
  18. data/db/migrate/20260312000001_create_standard_id_consent_grants.rb +19 -0
  19. data/db/migrate/20260312000002_create_standard_id_revoked_tokens.rb +16 -0
  20. data/lib/generators/standard_id/oidc/install/install_generator.rb +149 -0
  21. data/lib/generators/standard_id/oidc/install/templates/initializer.rb.erb +56 -0
  22. data/lib/standard_id/oidc/config/schema.rb +17 -0
  23. data/lib/standard_id/oidc/engine.rb +36 -0
  24. data/lib/standard_id/oidc/extensions/authorization_code_flow_ext.rb +13 -0
  25. data/lib/standard_id/oidc/extensions/authorization_flow_ext.rb +60 -0
  26. data/lib/standard_id/oidc/extensions/introspections_controller_ext.rb +60 -0
  27. data/lib/standard_id/oidc/extensions/token_grant_flow_ext.rb +38 -0
  28. data/lib/standard_id/oidc/extensions/traditional_code_grant_ext.rb +24 -0
  29. data/lib/standard_id/oidc/id_token_service.rb +40 -0
  30. data/lib/standard_id/oidc/version.rb +5 -0
  31. data/lib/standard_id/oidc.rb +22 -0
  32. data/lib/standard_id/provider.rb +10 -0
  33. data/lib/tasks/standard_id/oidc_tasks.rake +4 -0
  34. metadata +107 -0
@@ -0,0 +1,31 @@
1
+ module StandardId
2
+ module Oidc
3
+ class RevokedToken < ApplicationRecord
4
+ self.table_name = "standard_id_revoked_tokens"
5
+
6
+ validates :jti, presence: true, uniqueness: true
7
+
8
+ scope :active, -> { where("expires_at > ?", Time.current) }
9
+
10
+ def self.revoke!(jti:, client_id: nil, token_type: nil, expires_at: nil)
11
+ create!(
12
+ jti: jti,
13
+ client_id: client_id,
14
+ token_type: token_type,
15
+ revoked_at: Time.current,
16
+ expires_at: expires_at || 1.day.from_now
17
+ )
18
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
19
+ # Token already revoked — idempotent
20
+ end
21
+
22
+ def self.revoked?(jti)
23
+ where(jti: jti).exists?
24
+ end
25
+
26
+ def self.cleanup_expired!
27
+ where("expires_at <= ?", Time.current).delete_all
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Standard id provider</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "standard_id/oidc/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,15 @@
1
+ StandardId::Oidc::Engine.routes.draw do
2
+ scope ".well-known" do
3
+ get "openid-configuration", to: "discovery#show", as: :openid_configuration
4
+ end
5
+
6
+ scope "api/provider" do
7
+ # NOTE: there is deliberately no `introspect` route here any more. Core's
8
+ # RFC 7662 endpoint (POST /oauth/introspect, gated on
9
+ # `config.oauth.introspection_enabled`) is the one endpoint, and this engine
10
+ # contributes its denylist into it — see
11
+ # StandardId::Oidc::Extensions::IntrospectionsControllerExt.
12
+ post "revoke", to: "revocation#create", as: :revoke
13
+ resource :consent, only: [ :show, :create, :destroy ], controller: :consent
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ class CreateStandardIdConsentGrants < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_consent_grants do |t|
4
+ t.references :account, null: false
5
+ t.references :client_application, null: false
6
+ t.string :scopes, null: false
7
+ t.datetime :granted_at, null: false
8
+ t.datetime :revoked_at
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :standard_id_consent_grants,
14
+ [ :account_id, :client_application_id ],
15
+ unique: true,
16
+ where: "revoked_at IS NULL",
17
+ name: "idx_consent_grants_active_unique"
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ class CreateStandardIdRevokedTokens < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_revoked_tokens do |t|
4
+ t.string :jti, null: false
5
+ t.string :token_type
6
+ t.string :client_id
7
+ t.datetime :revoked_at, null: false
8
+ t.datetime :expires_at, null: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :standard_id_revoked_tokens, :jti, unique: true
14
+ add_index :standard_id_revoked_tokens, :expires_at
15
+ end
16
+ end
@@ -0,0 +1,149 @@
1
+ require "rails/generators"
2
+
3
+ module StandardId
4
+ module Oidc
5
+ module Generators
6
+ # Installs StandardId::Oidc in a host Rails application.
7
+ #
8
+ # Three steps, each independently skippable:
9
+ #
10
+ # * copies the engine's migrations into db/migrate/
11
+ # * writes config/initializers/standard_id_oidc.rb
12
+ # * mounts the engine in config/routes.rb
13
+ #
14
+ # Idempotent: re-running skips pieces that are already installed and says
15
+ # so. Pass `--skip-*` to opt out of individual steps and `--force` to
16
+ # overwrite an existing initializer.
17
+ class InstallGenerator < Rails::Generators::Base
18
+ source_root File.expand_path("templates", __dir__)
19
+
20
+ desc <<~DESC
21
+ Installs StandardId::Oidc. By default this:
22
+ * copies the engine's migrations into db/migrate/
23
+ * writes config/initializers/standard_id_oidc.rb
24
+ * mounts StandardId::Oidc::Engine in config/routes.rb
25
+
26
+ Use --skip-* flags to opt out of individual steps when re-running on
27
+ an existing install. The generator is idempotent — already-installed
28
+ pieces are skipped with a clear message. Pass --force to overwrite an
29
+ existing initializer.
30
+
31
+ Run `rails db:migrate` afterwards.
32
+ DESC
33
+
34
+ LEGACY_INITIALIZER = "config/initializers/standard_id_provider.rb"
35
+
36
+ class_option :skip_migrations, type: :boolean, default: false,
37
+ desc: "Do not copy the engine's migrations into db/migrate"
38
+ class_option :skip_initializer, type: :boolean, default: false,
39
+ desc: "Do not write config/initializers/standard_id_oidc.rb"
40
+ class_option :skip_routes, type: :boolean, default: false,
41
+ desc: "Do not mount StandardId::Oidc::Engine in config/routes.rb"
42
+ class_option :mount_path, type: :string, default: "/",
43
+ desc: "Path to mount StandardId::Oidc::Engine at"
44
+ class_option :force, type: :boolean, default: false,
45
+ desc: "Overwrite config/initializers/standard_id_oidc.rb if it already exists"
46
+
47
+ # Copies the engine's migrations with the same `.standard_id_oidc`
48
+ # suffix `rails standard_id_oidc:install:migrations` uses, so the
49
+ # two are interchangeable and neither double-installs the other's work.
50
+ # Done in-process rather than by shelling out to that rake task so the
51
+ # generator stays testable and does not need a booted host app.
52
+ # Migrations copied under the gem's former name (`.standard_id_provider`
53
+ # suffix) count as installed, so an upgraded host is not double-migrated.
54
+ def copy_migrations
55
+ if options[:skip_migrations]
56
+ say_status("skip", "db/migrate (--skip-migrations)", :yellow)
57
+ return
58
+ end
59
+
60
+ if existing_migrations.any?
61
+ say_status(
62
+ "identical",
63
+ "StandardId::Oidc migrations already present (#{existing_migrations.size}), skipping",
64
+ :blue
65
+ )
66
+ return
67
+ end
68
+
69
+ engine_migrations.each_with_index do |source, index|
70
+ name = File.basename(source, ".rb").sub(/\A\d+_/, "")
71
+ copy_file source, "db/migrate/#{migration_number(index)}_#{name}.standard_id_oidc.rb"
72
+ end
73
+ end
74
+
75
+ def copy_initializer
76
+ path = "config/initializers/standard_id_oidc.rb"
77
+
78
+ if options[:skip_initializer]
79
+ say_status("skip", "#{path} (--skip-initializer)", :yellow)
80
+ return
81
+ end
82
+
83
+ if File.exist?(File.join(destination_root, LEGACY_INITIALIZER)) && !options[:force]
84
+ say_status("identical", "#{LEGACY_INITIALIZER} (former name; rename it at your leisure)", :blue)
85
+ return
86
+ end
87
+
88
+ if File.exist?(File.join(destination_root, path)) && !options[:force]
89
+ say_status("identical", "#{path} (already exists; pass --force to overwrite)", :blue)
90
+ return
91
+ end
92
+
93
+ template "initializer.rb.erb", path, force: options[:force]
94
+ end
95
+
96
+ def mount_engine
97
+ routes_path = "config/routes.rb"
98
+
99
+ if options[:skip_routes]
100
+ say_status("skip", "#{routes_path} (--skip-routes)", :yellow)
101
+ return
102
+ end
103
+
104
+ unless File.exist?(File.join(destination_root, routes_path))
105
+ say_status("warn", "#{routes_path} not found; mount the engine yourself", :red)
106
+ return
107
+ end
108
+
109
+ # StandardId::Provider is an alias of StandardId::Oidc, so a legacy
110
+ # mount is the same engine; mounting it again would duplicate routes.
111
+ if File.read(File.join(destination_root, routes_path)).match?(/StandardId::(Oidc|Provider)::Engine/)
112
+ say_status("identical", "#{routes_path} (engine already mounted)", :blue)
113
+ return
114
+ end
115
+
116
+ route(%(mount StandardId::Oidc::Engine => "#{options[:mount_path]}"))
117
+ end
118
+
119
+ def print_next_steps
120
+ say ""
121
+ say "StandardId::Oidc installed. Next:", :green
122
+ say " 1. rails db:migrate"
123
+ say " 2. Set config.oauth.discovery_endpoint_base in your StandardId"
124
+ say " initializer — this engine's discovery document cannot detect"
125
+ say " the ApiEngine mount on its own. See the README."
126
+ say " 3. Set config.oauth.introspection_enabled = true if you want the"
127
+ say " RFC 7662 endpoint (it is off by default in standard_id)."
128
+ say ""
129
+ end
130
+
131
+ no_commands do
132
+ def existing_migrations
133
+ Dir.glob(File.join(destination_root, "db/migrate/*.{standard_id_oidc,standard_id_provider}.rb"))
134
+ end
135
+
136
+ def engine_migrations
137
+ Dir.glob(File.expand_path("../../../../../db/migrate/*.rb", __dir__)).sort
138
+ end
139
+
140
+ # Migrations are ordered among themselves and must not collide, hence
141
+ # the index offset. `install:migrations` does the same thing.
142
+ def migration_number(index)
143
+ (Time.now.utc + index).strftime("%Y%m%d%H%M%S")
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,56 @@
1
+ # StandardId::Oidc — OpenID Connect Identity Provider addon for StandardId.
2
+ #
3
+ # This engine EXTENDS standard_id rather than standing alone: it prepends into
4
+ # StandardId's OAuth flow classes and contributes its token denylist into
5
+ # StandardId's introspection endpoint. Your `standard_id.rb` initializer is
6
+ # still the primary configuration surface; the settings below are the ones this
7
+ # engine adds, plus the two core settings a provider deployment must revisit.
8
+
9
+ StandardId.configure do |c|
10
+ # --- Settings this engine adds -------------------------------------------
11
+
12
+ # Lifetime of issued ID tokens, in seconds.
13
+ # c.provider.id_token_lifetime = 3600
14
+
15
+ # Advertised in the discovery document. These describe what YOU are willing
16
+ # to issue — narrow them rather than widen them.
17
+ # c.provider.scopes_supported = %w[openid profile email offline_access]
18
+ # c.provider.claims_supported = %w[sub iss aud exp iat nonce auth_time at_hash email name email_verified]
19
+ # c.provider.subject_types_supported = %w[public]
20
+
21
+ # RFC 7009 token revocation at POST <provider mount>/api/provider/revoke.
22
+ # When false the endpoint renders 404, indistinguishable from one that does
23
+ # not exist. This is the endpoint that writes the RevokedToken denylist, so
24
+ # turning it off means access tokens cannot be revoked before their exp.
25
+ # c.provider.revocation_enabled = true
26
+
27
+ # --- Core settings a provider deployment must revisit --------------------
28
+
29
+ # REQUIRED for discovery. The issuer is a stable security identifier
30
+ # (RFC 8414 §2); clients match it byte-for-byte against their discovery URL
31
+ # and against the `iss` claim of issued tokens.
32
+ # c.issuer = "https://auth.example.com"
33
+
34
+ # REQUIRED for discovery, and easy to miss. This engine's discovery document
35
+ # is served from the PROVIDER engine's mount, so — unlike core's own
36
+ # well-known controllers — it cannot read the ApiEngine mount out of
37
+ # SCRIPT_NAME. Tell it where the endpoints actually live, or the document
38
+ # will advertise URLs that 404.
39
+ #
40
+ # c.oauth.discovery_endpoint_base = ->(request:) { "#{request.base_url}/api" }
41
+ # # or, for a fixed deployment:
42
+ # c.oauth.discovery_endpoint_base = "https://auth.example.com/api"
43
+
44
+ # RFC 7662 introspection lives on CORE's endpoint (POST /oauth/introspect),
45
+ # which is OFF by default — it 404s until you opt in. This engine contributes
46
+ # its RevokedToken denylist into it, so a revoked access token introspects as
47
+ # inactive rather than staying active until its exp.
48
+ # c.oauth.introspection_enabled = true
49
+
50
+ # Members that cannot be derived — a host-owned authorization shim, a
51
+ # deliberately narrower scope list. Applied LAST, so they win over everything
52
+ # above. Setting `issuer` here raises; use c.issuer.
53
+ # c.oauth.discovery_metadata_overrides = {
54
+ # authorization_endpoint: ->(ctx) { "#{ctx[:origin]}/oauth/authorize" }
55
+ # }
56
+ end
@@ -0,0 +1,17 @@
1
+ StandardId::ConfigSchema.define do
2
+ scope :provider do
3
+ field :id_token_lifetime, type: :integer, default: 3600
4
+ field :scopes_supported, type: :array, default: -> { %w[openid profile email offline_access] }
5
+ field :claims_supported, type: :array, default: -> { %w[sub iss aud exp iat nonce auth_time at_hash email name email_verified] }
6
+ field :subject_types_supported, type: :array, default: -> { %w[public] }
7
+ # NOTE: there is deliberately no `introspection_enabled` here any more.
8
+ # It was declared, documented as a switch, and never read by anything —
9
+ # the endpoint it claimed to gate was always on. Introspection is now
10
+ # core's endpoint, gated by `StandardId.config.oauth.introspection_enabled`
11
+ # (which defaults to FALSE — a provider deployment must opt in).
12
+ #
13
+ # `revocation_enabled` had the same defect and is now actually enforced by
14
+ # StandardId::Oidc::RevocationController.
15
+ field :revocation_enabled, type: :boolean, default: true
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module StandardId
2
+ module Oidc
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace StandardId::Oidc
5
+
6
+ initializer "standard_id_oidc.extend_core" do
7
+ StandardId::Oauth::TokenGrantFlow.prepend(
8
+ StandardId::Oidc::Extensions::TokenGrantFlowExt
9
+ )
10
+
11
+ StandardId::Oauth::AuthorizationCodeAuthorizationFlow.prepend(
12
+ StandardId::Oidc::Extensions::AuthorizationFlowExt
13
+ )
14
+
15
+ StandardId::Oauth::Subflows::TraditionalCodeGrant.prepend(
16
+ StandardId::Oidc::Extensions::TraditionalCodeGrantExt
17
+ )
18
+
19
+ StandardId::Oauth::AuthorizationCodeFlow.prepend(
20
+ StandardId::Oidc::Extensions::AuthorizationCodeFlowExt
21
+ )
22
+ end
23
+
24
+ # Controllers are reloadable, so this cannot live in the initializer
25
+ # above: referencing the constant there would pin the boot-time class and
26
+ # every `reload!` in development would drop the extension. `to_prepare`
27
+ # runs on each reload, and `prepend` of an already-prepended module is a
28
+ # no-op, so re-running it is free.
29
+ config.to_prepare do
30
+ StandardId::Api::Oauth::IntrospectionsController.prepend(
31
+ StandardId::Oidc::Extensions::IntrospectionsControllerExt
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module StandardId
2
+ module Oidc
3
+ module Extensions
4
+ module AuthorizationCodeFlowExt
5
+ private
6
+
7
+ def id_token_nonce
8
+ @authorization_code&.nonce
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ module StandardId
2
+ module Oidc
3
+ module Extensions
4
+ module AuthorizationFlowExt
5
+ private
6
+
7
+ def generate_authorization_response
8
+ if consent_required? && !consent_granted?
9
+ return redirect_to_consent
10
+ end
11
+
12
+ super
13
+ end
14
+
15
+ def common_subflow_params(flow_params)
16
+ super.merge(nonce: flow_params[:nonce])
17
+ end
18
+
19
+ def consent_required?
20
+ @client&.require_consent? && openid_scope_requested?
21
+ end
22
+
23
+ def openid_scope_requested?
24
+ scope.to_s.split(/\s+/).include?("openid")
25
+ end
26
+
27
+ def consent_granted?
28
+ return false unless current_account
29
+
30
+ grant = StandardId::Oidc::ConsentGrant.active
31
+ .find_by(account: current_account, client_application: @client)
32
+
33
+ return false unless grant
34
+
35
+ requested = scope.to_s.split(/\s+/).sort
36
+ granted = grant.scopes.to_s.split(/\s+/).sort
37
+ (requested - granted).empty?
38
+ end
39
+
40
+ def redirect_to_consent
41
+ consent_params = {
42
+ client_id: params[:client_id],
43
+ redirect_uri: redirect_uri,
44
+ scope: scope,
45
+ state: state,
46
+ audience: audience,
47
+ nonce: params[:nonce],
48
+ response_type: params[:response_type],
49
+ code_challenge: params[:code_challenge],
50
+ code_challenge_method: params[:code_challenge_method]
51
+ }.compact
52
+
53
+ consent_url = StandardId::Oidc::Engine.routes.url_helpers.consent_path(consent_params)
54
+
55
+ { redirect_to: consent_url, status: :found }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ module StandardId
2
+ module Oidc
3
+ module Extensions
4
+ # Teaches core's RFC 7662 endpoint (`POST /oauth/introspect`,
5
+ # StandardId::Api::Oauth::IntrospectionsController) about this engine's
6
+ # access-token denylist.
7
+ #
8
+ # ## Why this replaced a second introspection endpoint
9
+ #
10
+ # This gem used to ship its own `POST /api/provider/introspect`. Since
11
+ # standard_id 0.33.0 core has one too, and core's is strictly more
12
+ # RFC-conformant:
13
+ #
14
+ # * RFC 7662 §2.2 — every failure mode renders `{"active": false}` and
15
+ # NOTHING else. The provider's raised InvalidClientError on bad client
16
+ # credentials, answering 401 with an error body: a caller could tell
17
+ # "your credentials are wrong" from "that token is not valid".
18
+ # * The provider's rate limiter answered 429, which turns the throttle
19
+ # into a token-validity oracle — probe until throttled, then read the
20
+ # status code rather than the body. Core's renders the ordinary
21
+ # inactive response instead, deliberately.
22
+ # * Core's is 404-gated behind `config.oauth.introspection_enabled`, so
23
+ # an endpoint that answers questions about other people's tokens is
24
+ # not exposed by accident. The provider's was always on — and its own
25
+ # `config.provider.introspection_enabled` flag was never read.
26
+ #
27
+ # Only one thing was worth keeping: core cannot see
28
+ # StandardId::Oidc::RevokedToken. Core says so itself — access tokens
29
+ # are stateless, "this engine cannot invalidate a stateless JWT before its
30
+ # exp". This engine's revocation endpoint maintains exactly the jti
31
+ # denylist that makes that possible. So rather than run two endpoints with
32
+ # different answers, the denylist is contributed INTO core's.
33
+ #
34
+ # ## Coupling
35
+ #
36
+ # `#decode_token` is a private method of a non-public controller. That is
37
+ # the same bargain the rest of this gem already makes (see
38
+ # StandardId::Oidc::Engine), and the reason the gemspec pins
39
+ # `standard_id` to `~> 0.33` with a `compat` CI job watching it.
40
+ #
41
+ # Returning nil is the whole mechanism: core already treats a nil decode
42
+ # as inactive and renders the bare `{"active": false}`, so a denylisted
43
+ # token is indistinguishable from a forged one — which is what RFC 7662
44
+ # §2.2 wants.
45
+ module IntrospectionsControllerExt
46
+ private
47
+
48
+ def decode_token(token)
49
+ payload = super
50
+ return nil if payload.nil?
51
+
52
+ jti = payload[:jti]
53
+ return nil if jti.present? && StandardId::Oidc::RevokedToken.revoked?(jti)
54
+
55
+ payload
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ module StandardId
2
+ module Oidc
3
+ module Extensions
4
+ module TokenGrantFlowExt
5
+ private
6
+
7
+ def generate_token_response
8
+ response = super
9
+
10
+ if openid_scope_requested? && subject_id.present?
11
+ response[:id_token] = Oidc::IdTokenService.generate(
12
+ subject_id: subject_id,
13
+ client_id: client_id,
14
+ nonce: id_token_nonce,
15
+ access_token: response[:access_token],
16
+ auth_time: id_token_auth_time
17
+ )
18
+ end
19
+
20
+ response
21
+ end
22
+
23
+ def build_jwt_payload(expires_in)
24
+ payload = super
25
+ payload[:jti] = SecureRandom.uuid
26
+ payload
27
+ end
28
+
29
+ def openid_scope_requested?
30
+ current_scopes.include?("openid")
31
+ end
32
+
33
+ def id_token_nonce = nil
34
+ def id_token_auth_time = nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ module StandardId
2
+ module Oidc
3
+ module Extensions
4
+ module TraditionalCodeGrantExt
5
+ private
6
+
7
+ def store_authorization_code
8
+ StandardId::AuthorizationCode.issue!(
9
+ plaintext_code: authorization_code,
10
+ client_id: params[:client_id],
11
+ redirect_uri: params[:redirect_uri],
12
+ scope: params[:scope],
13
+ audience: params[:audience],
14
+ account: params[:current_account],
15
+ code_challenge: params[:code_challenge],
16
+ code_challenge_method: params[:code_challenge_method],
17
+ nonce: params[:nonce],
18
+ metadata: { state: params[:state] }.compact
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ module StandardId
2
+ module Oidc
3
+ class IdTokenService
4
+ class << self
5
+ def generate(subject_id:, client_id:, nonce: nil, access_token: nil, authorization_code: nil, auth_time: nil, extra_claims: {})
6
+ payload = {
7
+ sub: subject_id,
8
+ aud: client_id,
9
+ nonce: nonce,
10
+ auth_time: auth_time&.to_i
11
+ }.compact
12
+
13
+ payload[:at_hash] = compute_half_hash(access_token) if access_token.present?
14
+ payload[:c_hash] = compute_half_hash(authorization_code) if authorization_code.present?
15
+ payload.merge!(extra_claims.symbolize_keys) if extra_claims.present?
16
+
17
+ expires_in = StandardId.config.provider.id_token_lifetime.seconds
18
+
19
+ StandardId::JwtService.encode(payload, expires_in: expires_in)
20
+ end
21
+
22
+ private
23
+
24
+ def compute_half_hash(value)
25
+ digest = hash_algorithm.digest(value)
26
+ Base64.urlsafe_encode64(digest[0, digest.length / 2], padding: false)
27
+ end
28
+
29
+ def hash_algorithm
30
+ case StandardId::JwtService.algorithm
31
+ when /256$/ then OpenSSL::Digest::SHA256
32
+ when /384$/ then OpenSSL::Digest::SHA384
33
+ when /512$/ then OpenSSL::Digest::SHA512
34
+ else OpenSSL::Digest::SHA256
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module StandardId
2
+ module Oidc
3
+ VERSION = "0.6.0"
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ require "standard_id"
2
+
3
+ require "standard_id/oidc/version"
4
+ require "standard_id/oidc/config/schema"
5
+ require "standard_id/oidc/id_token_service"
6
+ require "standard_id/oidc/extensions/token_grant_flow_ext"
7
+ require "standard_id/oidc/extensions/authorization_flow_ext"
8
+ require "standard_id/oidc/extensions/traditional_code_grant_ext"
9
+ require "standard_id/oidc/extensions/authorization_code_flow_ext"
10
+ require "standard_id/oidc/extensions/introspections_controller_ext"
11
+ require "standard_id/oidc/engine"
12
+
13
+ module StandardId
14
+ module Oidc
15
+ end
16
+
17
+ # Former name (the gem was standard_id-provider). Kept so existing host code
18
+ # (`StandardId::Provider::Engine` in routes, `StandardId::Provider::ConsentGrant`)
19
+ # keeps loading; deprecated, and removed in a future minor release.
20
+ Provider = Oidc
21
+ deprecate_constant :Provider
22
+ end
@@ -0,0 +1,10 @@
1
+ # Compatibility shim for the gem's former name, standard_id-provider.
2
+ #
3
+ # `require "standard_id/provider"` keeps working. The `StandardId::Provider`
4
+ # constant alias itself lives in standard_id/oidc.rb, so it is available however
5
+ # the gem was loaded. Both are deprecated: use `require "standard_id/oidc"` and
6
+ # `StandardId::Oidc`. This shim will be removed in a future minor release.
7
+ warn "[standard_id-oidc] `require \"standard_id/provider\"` is deprecated; " \
8
+ "use `require \"standard_id/oidc\"` instead.", uplevel: 1, category: :deprecated
9
+
10
+ require "standard_id/oidc"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :standard_id_oidc do
3
+ # # Task goes here
4
+ # end