standard_id-provider 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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/standard_id/provider/application.css +15 -0
  6. data/app/controllers/standard_id/provider/application_controller.rb +40 -0
  7. data/app/controllers/standard_id/provider/consent_controller.rb +91 -0
  8. data/app/controllers/standard_id/provider/discovery_controller.rb +34 -0
  9. data/app/controllers/standard_id/provider/introspection_controller.rb +41 -0
  10. data/app/controllers/standard_id/provider/revocation_controller.rb +29 -0
  11. data/app/helpers/standard_id/provider/application_helper.rb +6 -0
  12. data/app/jobs/standard_id/provider/application_job.rb +6 -0
  13. data/app/mailers/standard_id/provider/application_mailer.rb +8 -0
  14. data/app/models/standard_id/provider/application_record.rb +7 -0
  15. data/app/models/standard_id/provider/consent_grant.rb +41 -0
  16. data/app/models/standard_id/provider/revoked_token.rb +31 -0
  17. data/app/views/layouts/standard_id/provider/application.html.erb +17 -0
  18. data/config/routes.rb +11 -0
  19. data/db/migrate/20260312000001_create_standard_id_consent_grants.rb +19 -0
  20. data/db/migrate/20260312000002_create_standard_id_revoked_tokens.rb +16 -0
  21. data/lib/standard_id/provider/config/schema.rb +10 -0
  22. data/lib/standard_id/provider/engine.rb +25 -0
  23. data/lib/standard_id/provider/extensions/authorization_code_flow_ext.rb +13 -0
  24. data/lib/standard_id/provider/extensions/authorization_flow_ext.rb +60 -0
  25. data/lib/standard_id/provider/extensions/token_grant_flow_ext.rb +38 -0
  26. data/lib/standard_id/provider/extensions/traditional_code_grant_ext.rb +24 -0
  27. data/lib/standard_id/provider/id_token_service.rb +40 -0
  28. data/lib/standard_id/provider/version.rb +5 -0
  29. data/lib/standard_id/provider.rb +15 -0
  30. data/lib/tasks/standard_id/provider_tasks.rake +4 -0
  31. metadata +102 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7277bed83877f6bf4d594edaf754208a194edc113b166428005491855f5c4e94
4
+ data.tar.gz: 6bb65c12882616a7b5116e56d82b6aae805ffcce9e021621f2572a00f4c4f4a3
5
+ SHA512:
6
+ metadata.gz: f1648a8f251196809c60e44ab87dc8611e943f73c27e5430b70660c65af010eb523ac8dcce958ec992786e3784e571a67b49dc057ed1475079d8fd635d5410c1
7
+ data.tar.gz: 5ee6aaf6993590ddbf1c2139c26d11c079d0b3aee394cf38c48fe05b5cebaf85e36bbbefed4b8e641e9cfc63d657b66349e0442904b08db3ab95519d9566ac89
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Jaryl Sim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # StandardId::Provider
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "standard_id-provider"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install standard_id-provider
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,40 @@
1
+ module StandardId
2
+ module Provider
3
+ class ApplicationController < ActionController::API
4
+ rescue_from StandardId::OAuthError do |e|
5
+ render json: { error: e.oauth_error_code, error_description: e.message }, status: e.http_status
6
+ end
7
+
8
+ private
9
+
10
+ def authenticate_client!
11
+ extract_client_credentials_from_basic_auth
12
+
13
+ @client_credential = StandardId::ClientSecretCredential.active.find_by(client_id: params[:client_id])
14
+
15
+ unless @client_credential&.authenticate_client_secret(params[:client_secret])
16
+ raise StandardId::InvalidClientError, "Client authentication failed"
17
+ end
18
+
19
+ @client_credential
20
+ end
21
+
22
+ def extract_client_credentials_from_basic_auth
23
+ auth_header = request.headers["Authorization"]
24
+ return unless auth_header&.start_with?("Basic ")
25
+
26
+ if params[:client_id].present? || params[:client_secret].present?
27
+ raise StandardId::InvalidRequestError,
28
+ "Client credentials must be sent via Authorization header OR request body, not both"
29
+ end
30
+
31
+ decoded = Base64.strict_decode64(auth_header.split(" ", 2).last)
32
+ client_id, client_secret = decoded.split(":", 2)
33
+ params[:client_id] = CGI.unescape(client_id)
34
+ params[:client_secret] = CGI.unescape(client_secret)
35
+ rescue ArgumentError
36
+ raise StandardId::InvalidClientError, "Invalid Basic authentication encoding"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,91 @@
1
+ module StandardId
2
+ module Provider
3
+ class ConsentController < ApplicationController
4
+ include ActionController::Cookies
5
+
6
+ before_action :require_authentication
7
+ before_action :load_client
8
+
9
+ def show
10
+ render json: {
11
+ client: { name: @client.name, description: @client.description },
12
+ scopes: requested_scopes,
13
+ authorization_params: authorization_params
14
+ }
15
+ end
16
+
17
+ def create
18
+ ConsentGrant.grant!(
19
+ account: current_account,
20
+ client_application: @client,
21
+ scopes: authorization_params[:scope]
22
+ )
23
+
24
+ authorize_url = build_authorize_redirect
25
+ redirect_to authorize_url, allow_other_host: true, status: :found
26
+ end
27
+
28
+ def destroy
29
+ redirect_uri = authorization_params[:redirect_uri]
30
+
31
+ if redirect_uri.present?
32
+ deny_url = build_error_redirect(redirect_uri, "access_denied", "The user denied the consent request")
33
+ redirect_to deny_url, allow_other_host: true, status: :found
34
+ else
35
+ render json: { error: "access_denied", error_description: "The user denied the consent request" }, status: :forbidden
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def require_authentication
42
+ return if current_account.present?
43
+
44
+ render json: { error: "login_required", error_description: "Authentication is required" }, status: :unauthorized
45
+ end
46
+
47
+ def current_account
48
+ @current_account ||= begin
49
+ token_manager = StandardId::Web::TokenManager.new(request)
50
+ session_manager = StandardId::Web::SessionManager.new(token_manager, request: request, session: session, cookies: cookies)
51
+ session_manager.current_account
52
+ end
53
+ end
54
+
55
+ def load_client
56
+ @client = StandardId::ClientApplication.active.find_by(client_id: authorization_params[:client_id])
57
+
58
+ unless @client
59
+ raise StandardId::InvalidClientError, "Invalid client_id"
60
+ end
61
+ end
62
+
63
+ def authorization_params
64
+ @authorization_params ||= params.permit(
65
+ :client_id, :redirect_uri, :scope, :state, :audience,
66
+ :nonce, :response_type, :code_challenge, :code_challenge_method
67
+ ).to_h.symbolize_keys
68
+ end
69
+
70
+ def requested_scopes
71
+ authorization_params[:scope].to_s.split(/\s+/)
72
+ end
73
+
74
+ def build_authorize_redirect
75
+ uri = URI.parse("#{StandardId.config.issuer}/api/authorize")
76
+ uri.query = URI.encode_www_form(authorization_params.compact)
77
+ uri.to_s
78
+ end
79
+
80
+ def build_error_redirect(redirect_uri, error, description)
81
+ uri = URI.parse(redirect_uri)
82
+ query = URI.decode_www_form(uri.query || "")
83
+ query << [ "error", error ]
84
+ query << [ "error_description", description ]
85
+ query << [ "state", authorization_params[:state] ] if authorization_params[:state].present?
86
+ uri.query = URI.encode_www_form(query)
87
+ uri.to_s
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,34 @@
1
+ module StandardId
2
+ module Provider
3
+ class DiscoveryController < ApplicationController
4
+ def show
5
+ render json: openid_configuration
6
+ end
7
+
8
+ private
9
+
10
+ def openid_configuration
11
+ issuer = StandardId.config.issuer
12
+ provider_config = StandardId.config.provider
13
+
14
+ {
15
+ issuer: issuer,
16
+ authorization_endpoint: "#{issuer}/api/authorize",
17
+ token_endpoint: "#{issuer}/api/oauth/token",
18
+ userinfo_endpoint: "#{issuer}/api/userinfo",
19
+ jwks_uri: "#{issuer}/api/.well-known/jwks.json",
20
+ introspection_endpoint: "#{issuer}/api/provider/introspect",
21
+ revocation_endpoint: "#{issuer}/api/provider/revoke",
22
+ scopes_supported: provider_config.scopes_supported,
23
+ response_types_supported: %w[code token],
24
+ grant_types_supported: %w[authorization_code client_credentials refresh_token],
25
+ subject_types_supported: provider_config.subject_types_supported,
26
+ id_token_signing_alg_values_supported: [ StandardId::JwtService.algorithm ],
27
+ token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
28
+ claims_supported: provider_config.claims_supported,
29
+ code_challenge_methods_supported: %w[S256 plain]
30
+ }.compact
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module StandardId
2
+ module Provider
3
+ class IntrospectionController < ApplicationController
4
+ before_action :authenticate_client!
5
+
6
+ def create
7
+ token = params[:token]
8
+
9
+ if token.blank?
10
+ render json: { active: false }
11
+ return
12
+ end
13
+
14
+ payload = StandardId::JwtService.decode(token)
15
+
16
+ if payload.nil?
17
+ render json: { active: false }
18
+ return
19
+ end
20
+
21
+ if payload[:jti].present? && RevokedToken.revoked?(payload[:jti])
22
+ render json: { active: false }
23
+ return
24
+ end
25
+
26
+ render json: {
27
+ active: true,
28
+ sub: payload[:sub],
29
+ client_id: payload[:client_id],
30
+ scope: payload[:scope],
31
+ iss: payload[:iss],
32
+ exp: payload[:exp],
33
+ iat: payload[:iat],
34
+ jti: payload[:jti],
35
+ aud: payload[:aud],
36
+ token_type: "Bearer"
37
+ }.compact
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module StandardId
2
+ module Provider
3
+ class RevocationController < ApplicationController
4
+ before_action :authenticate_client!
5
+
6
+ def create
7
+ token = params[:token]
8
+ return head :ok if token.blank?
9
+
10
+ payload = StandardId::JwtService.decode(token)
11
+ return head :ok if payload.nil?
12
+
13
+ jti = payload[:jti]
14
+ return head :ok if jti.blank?
15
+
16
+ expires_at = payload[:exp] ? Time.at(payload[:exp]) : 1.day.from_now
17
+
18
+ RevokedToken.revoke!(
19
+ jti: jti,
20
+ client_id: @client_credential.client_id,
21
+ token_type: params[:token_type_hint],
22
+ expires_at: expires_at
23
+ )
24
+
25
+ head :ok
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module StandardId
2
+ module Provider
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module StandardId
2
+ module Provider
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module StandardId
2
+ module Provider
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module StandardId
2
+ module Provider
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module StandardId
2
+ module Provider
3
+ class ConsentGrant < ApplicationRecord
4
+ self.table_name = "standard_id_consent_grants"
5
+
6
+ belongs_to :account, class_name: StandardId.config.account_class_name
7
+ belongs_to :client_application, class_name: "StandardId::ClientApplication"
8
+
9
+ validates :scopes, presence: true
10
+
11
+ scope :active, -> { where(revoked_at: nil) }
12
+ scope :revoked, -> { where.not(revoked_at: nil) }
13
+
14
+ before_validation :set_granted_at, on: :create
15
+
16
+ def self.grant!(account:, client_application:, scopes:)
17
+ transaction do
18
+ active.where(account: account, client_application: client_application).each(&:revoke!)
19
+ create!(account: account, client_application: client_application, scopes: scopes)
20
+ end
21
+ rescue ActiveRecord::RecordNotUnique
22
+ retry
23
+ end
24
+
25
+ def revoke!
26
+ update!(revoked_at: Time.current)
27
+ end
28
+
29
+ def covers_scopes?(requested_scopes)
30
+ granted = scopes.to_s.split(/\s+/)
31
+ (requested_scopes - granted).empty?
32
+ end
33
+
34
+ private
35
+
36
+ def set_granted_at
37
+ self.granted_at ||= Time.current
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ module StandardId
2
+ module Provider
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/provider/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ StandardId::Provider::Engine.routes.draw do
2
+ scope ".well-known" do
3
+ get "openid-configuration", to: "discovery#show"
4
+ end
5
+
6
+ scope "api/provider" do
7
+ post "introspect", to: "introspection#create"
8
+ post "revoke", to: "revocation#create"
9
+ resource :consent, only: [ :show, :create, :destroy ], controller: :consent
10
+ end
11
+ 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,10 @@
1
+ StandardConfig.schema.draw 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
+ field :introspection_enabled, type: :boolean, default: true
8
+ field :revocation_enabled, type: :boolean, default: true
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ module StandardId
2
+ module Provider
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace StandardId::Provider
5
+
6
+ initializer "standard_id_provider.extend_core" do
7
+ StandardId::Oauth::TokenGrantFlow.prepend(
8
+ StandardId::Provider::Extensions::TokenGrantFlowExt
9
+ )
10
+
11
+ StandardId::Oauth::AuthorizationCodeAuthorizationFlow.prepend(
12
+ StandardId::Provider::Extensions::AuthorizationFlowExt
13
+ )
14
+
15
+ StandardId::Oauth::Subflows::TraditionalCodeGrant.prepend(
16
+ StandardId::Provider::Extensions::TraditionalCodeGrantExt
17
+ )
18
+
19
+ StandardId::Oauth::AuthorizationCodeFlow.prepend(
20
+ StandardId::Provider::Extensions::AuthorizationCodeFlowExt
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module StandardId
2
+ module Provider
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 Provider
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::Provider::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::Provider::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,38 @@
1
+ module StandardId
2
+ module Provider
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] = Provider::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 Provider
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 Provider
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 Provider
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ require "standard_id"
2
+
3
+ require "standard_id/provider/version"
4
+ require "standard_id/provider/config/schema"
5
+ require "standard_id/provider/id_token_service"
6
+ require "standard_id/provider/extensions/token_grant_flow_ext"
7
+ require "standard_id/provider/extensions/authorization_flow_ext"
8
+ require "standard_id/provider/extensions/traditional_code_grant_ext"
9
+ require "standard_id/provider/extensions/authorization_code_flow_ext"
10
+ require "standard_id/provider/engine"
11
+
12
+ module StandardId
13
+ module Provider
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :standard_id_provider do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: standard_id-provider
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jaryl Sim
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: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: standard_id
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
40
+ description: 'Extends StandardId with full OIDC Identity Provider capabilities: ID
41
+ tokens, consent management, token introspection, token revocation, and discovery.'
42
+ email:
43
+ - code@jaryl.dev
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/assets/stylesheets/standard_id/provider/application.css
52
+ - app/controllers/standard_id/provider/application_controller.rb
53
+ - app/controllers/standard_id/provider/consent_controller.rb
54
+ - app/controllers/standard_id/provider/discovery_controller.rb
55
+ - app/controllers/standard_id/provider/introspection_controller.rb
56
+ - app/controllers/standard_id/provider/revocation_controller.rb
57
+ - app/helpers/standard_id/provider/application_helper.rb
58
+ - app/jobs/standard_id/provider/application_job.rb
59
+ - app/mailers/standard_id/provider/application_mailer.rb
60
+ - app/models/standard_id/provider/application_record.rb
61
+ - app/models/standard_id/provider/consent_grant.rb
62
+ - app/models/standard_id/provider/revoked_token.rb
63
+ - app/views/layouts/standard_id/provider/application.html.erb
64
+ - config/routes.rb
65
+ - db/migrate/20260312000001_create_standard_id_consent_grants.rb
66
+ - db/migrate/20260312000002_create_standard_id_revoked_tokens.rb
67
+ - lib/standard_id/provider.rb
68
+ - lib/standard_id/provider/config/schema.rb
69
+ - lib/standard_id/provider/engine.rb
70
+ - lib/standard_id/provider/extensions/authorization_code_flow_ext.rb
71
+ - lib/standard_id/provider/extensions/authorization_flow_ext.rb
72
+ - lib/standard_id/provider/extensions/token_grant_flow_ext.rb
73
+ - lib/standard_id/provider/extensions/traditional_code_grant_ext.rb
74
+ - lib/standard_id/provider/id_token_service.rb
75
+ - lib/standard_id/provider/version.rb
76
+ - lib/tasks/standard_id/provider_tasks.rake
77
+ homepage: https://github.com/rarebit-one/standard_id-provider
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/rarebit-one/standard_id-provider
82
+ source_code_uri: https://github.com/rarebit-one/standard_id-provider
83
+ changelog_uri: https://github.com/rarebit-one/standard_id-provider/blob/main/CHANGELOG.md
84
+ bug_tracker_uri: https://github.com/rarebit-one/standard_id-provider/issues
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '3.2'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 4.0.3
100
+ specification_version: 4
101
+ summary: OpenID Connect Identity Provider addon for StandardId.
102
+ test_files: []