kinde_sdk 1.4.0 → 1.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.
- checksums.yaml +4 -4
- data/kinde_api/spec/spec_helper.rb +1 -0
- data/lib/kinde_sdk/client.rb +1 -1
- data/lib/kinde_sdk/configuration.rb +8 -0
- data/lib/kinde_sdk/engine.rb +5 -0
- data/lib/kinde_sdk/version.rb +1 -1
- data/lib/kinde_sdk.rb +72 -1
- data/spec/kinde_sdk_spec.rb +44 -5
- data/spec/spec_helper.rb +3 -0
- metadata +45 -3
- data/lib/kinde_sdk/controllers/auth_controller.rb +0 -152
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 83f3ec0e5e4b6f61d409b0a6c6314459917c94fa6109440fda5634e7d2e747f6
         | 
| 4 | 
            +
              data.tar.gz: 35dd34872266b28076bf0a9a89f5bb624d6a314d4067fc359a49f901403ef7a2
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 58130b81b2d67b3c2ef8c4f34b6d87157016270b08af9d19d77bb67cfae9b27b32937bbd8b90891f0590b9ad7a7d4cdb0443cf4a2281983567c1960415f60bb8
         | 
| 7 | 
            +
              data.tar.gz: fa2f18f0219dbe76661f9a6809deeb0a1e5fcfde13ea300533d3be8c342c97902143392e1c9991e3b94ad5bfda319eefb042f21de27405e091d7b6fbb2d34c75
         | 
    
        data/lib/kinde_sdk/client.rb
    CHANGED
    
    
| @@ -10,6 +10,11 @@ module KindeSdk | |
| 10 10 | 
             
                attr_accessor :authorize_url
         | 
| 11 11 | 
             
                attr_accessor :token_url
         | 
| 12 12 |  | 
| 13 | 
            +
                attr_accessor :jwks_url
         | 
| 14 | 
            +
                attr_accessor :jwks
         | 
| 15 | 
            +
                attr_accessor :expected_issuer
         | 
| 16 | 
            +
                attr_accessor :expected_audience
         | 
| 17 | 
            +
             | 
| 13 18 | 
             
                attr_accessor :logger
         | 
| 14 19 | 
             
                attr_accessor :debugging
         | 
| 15 20 | 
             
                attr_accessor :oauth_client
         | 
| @@ -19,6 +24,9 @@ module KindeSdk | |
| 19 24 | 
             
                def initialize
         | 
| 20 25 | 
             
                  @authorize_url = '/oauth2/auth'
         | 
| 21 26 | 
             
                  @token_url = '/oauth2/token'
         | 
| 27 | 
            +
                  @jwks_url = '/.well-known/jwks.json'
         | 
| 28 | 
            +
                  @expected_audience = nil
         | 
| 29 | 
            +
                  @expected_issuer = nil
         | 
| 22 30 | 
             
                  @debugging = false
         | 
| 23 31 | 
             
                  @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
         | 
| 24 32 | 
             
                  @scope = 'openid offline email profile'
         | 
    
        data/lib/kinde_sdk/version.rb
    CHANGED
    
    
    
        data/lib/kinde_sdk.rb
    CHANGED
    
    | @@ -1,20 +1,28 @@ | |
| 1 1 | 
             
            require "logger"
         | 
| 2 | 
            +
            require "rails"
         | 
| 2 3 | 
             
            require "kinde_sdk/version"
         | 
| 3 4 | 
             
            require "kinde_sdk/configuration"
         | 
| 4 5 | 
             
            require "kinde_sdk/client/feature_flags"
         | 
| 5 6 | 
             
            require "kinde_sdk/client/permissions"
         | 
| 6 | 
            -
            require "kinde_sdk/controllers/auth_controller"
         | 
| 7 7 | 
             
            require "kinde_sdk/client"
         | 
| 8 8 | 
             
            require 'securerandom'
         | 
| 9 9 | 
             
            require 'oauth2'
         | 
| 10 10 | 
             
            require 'pkce_challenge'
         | 
| 11 11 | 
             
            require 'faraday/follow_redirects'
         | 
| 12 12 | 
             
            require 'uri'
         | 
| 13 | 
            +
            require 'httparty'
         | 
| 14 | 
            +
            require 'jwt'
         | 
| 15 | 
            +
            require 'openssl'
         | 
| 16 | 
            +
            require 'base64'
         | 
| 13 17 |  | 
| 14 18 | 
             
            module KindeSdk
         | 
| 15 19 | 
             
              class << self
         | 
| 16 20 | 
             
                attr_accessor :config
         | 
| 17 21 |  | 
| 22 | 
            +
                if defined?(Rails)
         | 
| 23 | 
            +
                  require "kinde_sdk/engine"
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 18 26 | 
             
                def configure
         | 
| 19 27 | 
             
                  if block_given?
         | 
| 20 28 | 
             
                    yield(Configuration.default)
         | 
| @@ -102,6 +110,8 @@ module KindeSdk | |
| 102 110 | 
             
                #
         | 
| 103 111 | 
             
                # @return [KindeSdk::Client]
         | 
| 104 112 | 
             
                def client(tokens_hash)
         | 
| 113 | 
            +
                  validate_jwt_token(tokens_hash)
         | 
| 114 | 
            +
                  
         | 
| 105 115 | 
             
                  sdk_api_client = api_client(tokens_hash[:access_token] || tokens_hash["access_token"])
         | 
| 106 116 | 
             
                  KindeSdk::Client.new(sdk_api_client, tokens_hash, @config.auto_refresh_tokens)
         | 
| 107 117 | 
             
                end
         | 
| @@ -135,6 +145,8 @@ module KindeSdk | |
| 135 145 | 
             
                  audience: "#{@config.domain}/api",
         | 
| 136 146 | 
             
                  domain: @config.domain
         | 
| 137 147 | 
             
                )
         | 
| 148 | 
            +
                  validate_jwt_token(hash)
         | 
| 149 | 
            +
                  
         | 
| 138 150 | 
             
                  OAuth2::AccessToken.from_hash(@config.oauth_client(
         | 
| 139 151 | 
             
                    client_id: client_id, 
         | 
| 140 152 | 
             
                    client_secret: client_secret,
         | 
| @@ -150,6 +162,8 @@ module KindeSdk | |
| 150 162 | 
             
                  audience: "#{@config.domain}/api",
         | 
| 151 163 | 
             
                  domain: @config.domain
         | 
| 152 164 | 
             
                )
         | 
| 165 | 
            +
                  validate_jwt_token(hash)
         | 
| 166 | 
            +
                  
         | 
| 153 167 | 
             
                  OAuth2::AccessToken.from_hash(@config.oauth_client(
         | 
| 154 168 | 
             
                    client_id: client_id, 
         | 
| 155 169 | 
             
                    client_secret: client_secret,
         | 
| @@ -182,5 +196,62 @@ module KindeSdk | |
| 182 196 | 
             
                rescue URI::InvalidURIError
         | 
| 183 197 | 
             
                  default_scheme
         | 
| 184 198 | 
             
                end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
             | 
| 201 | 
            +
                def validate_jwt_token(token_hash)
         | 
| 202 | 
            +
                  token_hash.each do |key, token|
         | 
| 203 | 
            +
                    next unless %w[access_token id_token].include?(key.to_s.downcase)
         | 
| 204 | 
            +
                    begin
         | 
| 205 | 
            +
                      jwt_validation(token, "#{@config.domain}#{@config.jwks_url}", @config.expected_issuer, @config.expected_audience)
         | 
| 206 | 
            +
                    rescue JWT::DecodeError
         | 
| 207 | 
            +
                      Rails.logger.error("Invalid JWT token: #{key}")
         | 
| 208 | 
            +
                      raise JWT::DecodeError, "Invalid #{key.to_s.capitalize.gsub('_', ' ')}"
         | 
| 209 | 
            +
                    end
         | 
| 210 | 
            +
                  end
         | 
| 211 | 
            +
                end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
             | 
| 214 | 
            +
                # Method to validate a JWT token with caching for JWKS
         | 
| 215 | 
            +
                def jwt_validation(jwt_token, jwks_url, expected_issuer, expected_audience)
         | 
| 216 | 
            +
                  @cached_jwks ||= fetch_jwks(jwks_url)
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  begin
         | 
| 219 | 
            +
                    validate_token(jwt_token, @cached_jwks, expected_issuer, expected_audience)
         | 
| 220 | 
            +
                  rescue JWT::DecodeError, StandardError
         | 
| 221 | 
            +
                    # If validation fails, fetch JWKS again and retry validation
         | 
| 222 | 
            +
                    @cached_jwks = fetch_jwks(jwks_url)
         | 
| 223 | 
            +
                    validate_token(jwt_token, @cached_jwks, expected_issuer, expected_audience)
         | 
| 224 | 
            +
                  end
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                private
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                # Fetch JWKS from the URL
         | 
| 230 | 
            +
                def fetch_jwks(jwks_url)
         | 
| 231 | 
            +
                  jwks_response = HTTParty.get(jwks_url)
         | 
| 232 | 
            +
                  JSON.parse(jwks_response.body)
         | 
| 233 | 
            +
                end
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                # Validate the JWT token using the provided JWKS
         | 
| 236 | 
            +
                def validate_token(jwt_token, jwks_hash, expected_issuer, expected_audience)
         | 
| 237 | 
            +
                  # Decode token header to get 'kid'
         | 
| 238 | 
            +
                  decoded_token = JWT.decode(jwt_token, nil, false) # [payload, header]
         | 
| 239 | 
            +
                  header = decoded_token[1]
         | 
| 240 | 
            +
                  kid = header['kid']
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                  # Find the matching JWK
         | 
| 243 | 
            +
                  jwks = JWT::JWK::Set.new(jwks_hash)
         | 
| 244 | 
            +
                  jwks.filter! {|key| key[:use] == 'sig' }
         | 
| 245 | 
            +
                  algorithms = jwks.map { |key| key[:alg] }.compact.uniq
         | 
| 246 | 
            +
                  payload, _header = JWT.decode(jwt_token, nil, true, algorithms: algorithms, jwks: jwks)
         | 
| 247 | 
            +
                  { valid: true, payload: payload }
         | 
| 248 | 
            +
                rescue JWT::DecodeError => e
         | 
| 249 | 
            +
                  Rails.logger.error("Token validation failed: #{e.message}")
         | 
| 250 | 
            +
                  raise JWT::DecodeError, "Token validation failed: #{e.message}"
         | 
| 251 | 
            +
                rescue StandardError => e
         | 
| 252 | 
            +
                  Rails.logger.error("Unexpected error: #{e.message}")
         | 
| 253 | 
            +
                  raise StandardError, "Unexpected error: #{e.message}"
         | 
| 254 | 
            +
                end
         | 
| 255 | 
            +
             | 
| 185 256 | 
             
              end
         | 
| 186 257 | 
             
            end
         | 
    
        data/spec/kinde_sdk_spec.rb
    CHANGED
    
    | @@ -1,4 +1,8 @@ | |
| 1 1 | 
             
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'jwt'
         | 
| 3 | 
            +
            require 'openssl'
         | 
| 4 | 
            +
            require 'webmock/rspec'
         | 
| 5 | 
            +
             | 
| 2 6 |  | 
| 3 7 | 
             
            describe KindeSdk do
         | 
| 4 8 | 
             
              let(:domain) { "http://example.com" }
         | 
| @@ -8,6 +12,13 @@ describe KindeSdk do | |
| 8 12 | 
             
              let(:logout_url) { "http://localhost/logout-callback" }
         | 
| 9 13 | 
             
              let(:auto_refresh_tokens) { true }
         | 
| 10 14 |  | 
| 15 | 
            +
              let(:optional_parameters) { { kid: 'my-kid', use: 'sig', alg: 'RS512' } }
         | 
| 16 | 
            +
              let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) }
         | 
| 17 | 
            +
              let(:jwk) { JWT::JWK.new(rsa_key, optional_parameters) }
         | 
| 18 | 
            +
              let(:payload) { { data: 'data' } }
         | 
| 19 | 
            +
              let(:token) { JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) }
         | 
| 20 | 
            +
              let(:jwks_hash) { JWT::JWK::Set.new(jwk).export }
         | 
| 21 | 
            +
             | 
| 11 22 | 
             
              before do
         | 
| 12 23 | 
             
                KindeSdk.configure do |c|
         | 
| 13 24 | 
             
                  c.domain = domain
         | 
| @@ -74,7 +85,19 @@ describe KindeSdk do | |
| 74 85 | 
             
                    )
         | 
| 75 86 | 
             
                    .to_return(
         | 
| 76 87 | 
             
                      status: 200,
         | 
| 77 | 
            -
                      body: { "access_token" | 
| 88 | 
            +
                      body: { "access_token" => "eyJ", "id_token" => "test", "refresh_token" => "test", "expires_in" => 86399, "scope" => "", "token_type" => "bearer" }.to_json,
         | 
| 89 | 
            +
                      headers: { "content-type" => "application/json;charset=UTF-8" }
         | 
| 90 | 
            +
                    )
         | 
| 91 | 
            +
                  stub_request(:get, "#{domain}/.well-known/jwks.json")
         | 
| 92 | 
            +
                    .with(
         | 
| 93 | 
            +
                      headers: {
         | 
| 94 | 
            +
                   	  'Accept'=>'*/*',
         | 
| 95 | 
            +
                   	  'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
         | 
| 96 | 
            +
                   	  'User-Agent'=>'Ruby'
         | 
| 97 | 
            +
                       })
         | 
| 98 | 
            +
                    .to_return(
         | 
| 99 | 
            +
                      status: 200,
         | 
| 100 | 
            +
                      body: jwks_hash.to_json,
         | 
| 78 101 | 
             
                      headers: { "content-type" => "application/json;charset=UTF-8" }
         | 
| 79 102 | 
             
                    )
         | 
| 80 103 | 
             
                end
         | 
| @@ -123,13 +146,13 @@ describe KindeSdk do | |
| 123 146 | 
             
                let(:hash_to_encode) do
         | 
| 124 147 | 
             
                  { "aud" => [],
         | 
| 125 148 | 
             
                    "azp" => "19ebb687cd2f405c9f2daf645a8db895",
         | 
| 126 | 
            -
                    "exp" => 1679600554,
         | 
| 127 149 | 
             
                    "feature_flags" => {
         | 
| 128 150 | 
             
                      "asd" => { "t" => "b", "v" => true },
         | 
| 129 151 | 
             
                      "eeeeee" => { "t" => "i", "v" => 111 },
         | 
| 130 152 | 
             
                      "qqq" => { "t" => "s", "v" => "aa" }
         | 
| 131 153 | 
             
                    },
         | 
| 132 | 
            -
                    "iat"  | 
| 154 | 
            +
                    "iat": Time.now.to_i,           # Issued at: current time
         | 
| 155 | 
            +
                    "exp": Time.now.to_i + 3600,  # Expiration time: 1 hour from now
         | 
| 133 156 | 
             
                    "iss" => "https://example.kinde.com",
         | 
| 134 157 | 
             
                    "jti" => "22c48b2c-da46-4661-a7ff-425c23eceab5",
         | 
| 135 158 | 
             
                    "org_code" => "org_cb4544175bc",
         | 
| @@ -137,9 +160,23 @@ describe KindeSdk do | |
| 137 160 | 
             
                    "scp" => ["openid", "offline"],
         | 
| 138 161 | 
             
                    "sub" => "kp:b17adf719f7d4b87b611d1a88a09fd15" }
         | 
| 139 162 | 
             
                end
         | 
| 140 | 
            -
                 | 
| 163 | 
            +
                before do
         | 
| 164 | 
            +
                  stub_request(:get, "#{domain}/.well-known/jwks.json")
         | 
| 165 | 
            +
                    .with(
         | 
| 166 | 
            +
                      headers: {
         | 
| 167 | 
            +
                   	  'Accept'=>'*/*',
         | 
| 168 | 
            +
                   	  'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
         | 
| 169 | 
            +
                   	  'User-Agent'=>'Ruby'
         | 
| 170 | 
            +
                       })
         | 
| 171 | 
            +
                    .to_return(
         | 
| 172 | 
            +
                      status: 200,
         | 
| 173 | 
            +
                      body: jwks_hash.to_json,
         | 
| 174 | 
            +
                      headers: { "content-type" => "application/json;charset=UTF-8" }
         | 
| 175 | 
            +
                    )
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
                let(:token) { JWT.encode(hash_to_encode, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) }
         | 
| 141 178 | 
             
                let(:expires_at) { Time.now.to_i + 10000000 }
         | 
| 142 | 
            -
                let(:client) { described_class.client({  | 
| 179 | 
            +
                let(:client) { described_class.client({ access_token: token, expires_at: expires_at }) }
         | 
| 143 180 |  | 
| 144 181 | 
             
                context "with feature flags" do
         | 
| 145 182 | 
             
                  it "returns existing flags", :aggregate_failures do
         | 
| @@ -229,3 +266,5 @@ describe KindeSdk do | |
| 229 266 | 
             
                end
         | 
| 230 267 | 
             
              end
         | 
| 231 268 | 
             
            end
         | 
| 269 | 
            +
             | 
| 270 | 
            +
             | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: kinde_sdk
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1. | 
| 4 | 
            +
              version: 1.6.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Kinde Australia Pty Ltd
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2025-03- | 
| 11 | 
            +
            date: 2025-03-27 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: typhoeus
         | 
| @@ -72,6 +72,48 @@ dependencies: | |
| 72 72 | 
             
                - - ">="
         | 
| 73 73 | 
             
                  - !ruby/object:Gem::Version
         | 
| 74 74 | 
             
                    version: '0'
         | 
| 75 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 76 | 
            +
              name: rails
         | 
| 77 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 78 | 
            +
                requirements:
         | 
| 79 | 
            +
                - - ">="
         | 
| 80 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 81 | 
            +
                    version: 7.0.4
         | 
| 82 | 
            +
              type: :runtime
         | 
| 83 | 
            +
              prerelease: false
         | 
| 84 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 85 | 
            +
                requirements:
         | 
| 86 | 
            +
                - - ">="
         | 
| 87 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 88 | 
            +
                    version: 7.0.4
         | 
| 89 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 90 | 
            +
              name: httparty
         | 
| 91 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 92 | 
            +
                requirements:
         | 
| 93 | 
            +
                - - "~>"
         | 
| 94 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 95 | 
            +
                    version: 0.19.0
         | 
| 96 | 
            +
              type: :runtime
         | 
| 97 | 
            +
              prerelease: false
         | 
| 98 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 99 | 
            +
                requirements:
         | 
| 100 | 
            +
                - - "~>"
         | 
| 101 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 102 | 
            +
                    version: 0.19.0
         | 
| 103 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 104 | 
            +
              name: jwt
         | 
| 105 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 106 | 
            +
                requirements:
         | 
| 107 | 
            +
                - - "~>"
         | 
| 108 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 109 | 
            +
                    version: '2.2'
         | 
| 110 | 
            +
              type: :runtime
         | 
| 111 | 
            +
              prerelease: false
         | 
| 112 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 113 | 
            +
                requirements:
         | 
| 114 | 
            +
                - - "~>"
         | 
| 115 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 116 | 
            +
                    version: '2.2'
         | 
| 75 117 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 76 118 | 
             
              name: rspec
         | 
| 77 119 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -422,7 +464,7 @@ files: | |
| 422 464 | 
             
            - lib/kinde_sdk/client/feature_flags.rb
         | 
| 423 465 | 
             
            - lib/kinde_sdk/client/permissions.rb
         | 
| 424 466 | 
             
            - lib/kinde_sdk/configuration.rb
         | 
| 425 | 
            -
            - lib/kinde_sdk/ | 
| 467 | 
            +
            - lib/kinde_sdk/engine.rb
         | 
| 426 468 | 
             
            - lib/kinde_sdk/version.rb
         | 
| 427 469 | 
             
            - spec/kinde_sdk_spec.rb
         | 
| 428 470 | 
             
            - spec/spec_helper.rb
         | 
| @@ -1,152 +0,0 @@ | |
| 1 | 
            -
            require 'action_controller'
         | 
| 2 | 
            -
            require 'uri'
         | 
| 3 | 
            -
            require 'cgi'
         | 
| 4 | 
            -
            require 'net/http'
         | 
| 5 | 
            -
            require 'json'
         | 
| 6 | 
            -
            require 'jwt'
         | 
| 7 | 
            -
             | 
| 8 | 
            -
            module KindeSdk
         | 
| 9 | 
            -
              class AuthController < ActionController::Base
         | 
| 10 | 
            -
                # Add before_action to validate nonce in callback
         | 
| 11 | 
            -
                before_action :validate_state, only: :callback
         | 
| 12 | 
            -
              
         | 
| 13 | 
            -
                def auth
         | 
| 14 | 
            -
                  # Generate a secure random nonce
         | 
| 15 | 
            -
                  nonce = SecureRandom.urlsafe_base64(16)
         | 
| 16 | 
            -
                  
         | 
| 17 | 
            -
                  # Call KindeSdk.auth_url with nonce
         | 
| 18 | 
            -
                  auth_data = KindeSdk.auth_url(nonce: nonce)
         | 
| 19 | 
            -
                  
         | 
| 20 | 
            -
                  # Store in session
         | 
| 21 | 
            -
                  session[:code_verifier] = auth_data[:code_verifier] if auth_data[:code_verifier].present?
         | 
| 22 | 
            -
                  session[:auth_nonce] = nonce
         | 
| 23 | 
            -
                  session[:auth_state] = {
         | 
| 24 | 
            -
                    requested_at: Time.current.to_i,
         | 
| 25 | 
            -
                    redirect_url: auth_data[:url]
         | 
| 26 | 
            -
                  }
         | 
| 27 | 
            -
              
         | 
| 28 | 
            -
                  redirect_to auth_data[:url], allow_other_host: true
         | 
| 29 | 
            -
                end
         | 
| 30 | 
            -
              
         | 
| 31 | 
            -
                def callback
         | 
| 32 | 
            -
                  tokens = KindeSdk.fetch_tokens(
         | 
| 33 | 
            -
                    params[:code],
         | 
| 34 | 
            -
                    code_verifier: KindeSdk.config.pkce_enabled ? session[:code_verifier] : nil
         | 
| 35 | 
            -
                  ).slice(:access_token, :id_token, :refresh_token, :expires_at)
         | 
| 36 | 
            -
              
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                  # Validate nonce in ID token
         | 
| 39 | 
            -
                  id_token = tokens[:id_token]
         | 
| 40 | 
            -
                  issuer = KindeSdk.config.domain
         | 
| 41 | 
            -
                  client_id = KindeSdk.config.client_id
         | 
| 42 | 
            -
                  original_nonce = session[:auth_nonce]
         | 
| 43 | 
            -
                  unless validate_nonce(id_token, original_nonce, issuer, client_id)
         | 
| 44 | 
            -
                    Rails.logger.warn("Nonce validation failed")
         | 
| 45 | 
            -
                    redirect_to "/", alert: "Invalid authentication nonce"
         | 
| 46 | 
            -
                    return
         | 
| 47 | 
            -
                  end
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                  # Store tokens and user in session
         | 
| 50 | 
            -
                  session[:kinde_auth] = OAuth2::AccessToken.from_hash(KindeSdk.config.oauth_client, tokens).to_hash
         | 
| 51 | 
            -
                    .slice(:access_token, :refresh_token, :expires_at)
         | 
| 52 | 
            -
                  session[:kinde_user] = KindeSdk.client(tokens).oauth.get_user.to_hash
         | 
| 53 | 
            -
                  
         | 
| 54 | 
            -
                  # Clear nonce and state after successful authentication
         | 
| 55 | 
            -
                  session.delete(:auth_nonce)
         | 
| 56 | 
            -
                  session.delete(:auth_state)
         | 
| 57 | 
            -
                  session.delete(:code_verifier)
         | 
| 58 | 
            -
                  redirect_to "/"
         | 
| 59 | 
            -
                rescue StandardError => e
         | 
| 60 | 
            -
                  Rails.logger.error("Authentication callback failed: #{e.message}")
         | 
| 61 | 
            -
                  redirect_to "/", alert: "Authentication failed"
         | 
| 62 | 
            -
                end
         | 
| 63 | 
            -
              
         | 
| 64 | 
            -
                def client_credentials_auth
         | 
| 65 | 
            -
                  result = KindeSdk.client_credentials_access(
         | 
| 66 | 
            -
                    client_id: ENV["KINDE_MANAGEMENT_CLIENT_ID"],
         | 
| 67 | 
            -
                    client_secret: ENV["KINDE_MANAGEMENT_CLIENT_SECRET"]
         | 
| 68 | 
            -
                  )
         | 
| 69 | 
            -
              
         | 
| 70 | 
            -
                  if result["error"].present?
         | 
| 71 | 
            -
                    Rails.logger.error("Client credentials auth failed: #{result['error']}")
         | 
| 72 | 
            -
                    raise result["error"]
         | 
| 73 | 
            -
                  end
         | 
| 74 | 
            -
              
         | 
| 75 | 
            -
                  $redis.set("kinde_m2m_token", result["access_token"], ex: result["expires_in"].to_i)
         | 
| 76 | 
            -
                  redirect_to mgmt_path
         | 
| 77 | 
            -
                end
         | 
| 78 | 
            -
              
         | 
| 79 | 
            -
                def logout
         | 
| 80 | 
            -
                  redirect_to KindeSdk.logout_url, allow_other_host: true
         | 
| 81 | 
            -
                end
         | 
| 82 | 
            -
              
         | 
| 83 | 
            -
                def logout_callback
         | 
| 84 | 
            -
                  reset_session
         | 
| 85 | 
            -
                  redirect_to "/"
         | 
| 86 | 
            -
                end
         | 
| 87 | 
            -
              
         | 
| 88 | 
            -
                private
         | 
| 89 | 
            -
              
         | 
| 90 | 
            -
                def validate_state
         | 
| 91 | 
            -
                  # Check if nonce and state exist in session
         | 
| 92 | 
            -
                  unless session[:auth_nonce] && session[:auth_state]
         | 
| 93 | 
            -
                    Rails.logger.warn("Missing session state or nonce")
         | 
| 94 | 
            -
                    redirect_to "/", alert: "Invalid authentication state"
         | 
| 95 | 
            -
                    return
         | 
| 96 | 
            -
                  end
         | 
| 97 | 
            -
              
         | 
| 98 | 
            -
                  # Verify nonce returned matches stored nonce
         | 
| 99 | 
            -
                  returned_state = params[:state]
         | 
| 100 | 
            -
                  stored_state = session[:auth_state]
         | 
| 101 | 
            -
                  stored_url = stored_state["redirect_url"]
         | 
| 102 | 
            -
              
         | 
| 103 | 
            -
                  # Extract the state from the stored redirect_url
         | 
| 104 | 
            -
                  parsed_url = URI.parse(stored_url)
         | 
| 105 | 
            -
                  query_params = CGI.parse(parsed_url.query || "")
         | 
| 106 | 
            -
                  stored_state_from_url = query_params["state"]&.first
         | 
| 107 | 
            -
             | 
| 108 | 
            -
                  # Verify returned state matches the state extracted from the redirect_url
         | 
| 109 | 
            -
                  unless returned_state.present? && returned_state == stored_state_from_url
         | 
| 110 | 
            -
                    Rails.logger.warn("State validation failed: returned=#{returned_state}, expected=#{stored_state_from_url}")
         | 
| 111 | 
            -
                    redirect_to "/", alert: "Invalid authentication state"
         | 
| 112 | 
            -
                    return
         | 
| 113 | 
            -
                  end
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                  # Optional: Check state age (e.g., expires after 15 minutes)
         | 
| 116 | 
            -
                  if Time.current.to_i - stored_state["requested_at"] > 900
         | 
| 117 | 
            -
                    Rails.logger.warn("Authentication state expired")
         | 
| 118 | 
            -
                    redirect_to "/", alert: "Authentication session expired"
         | 
| 119 | 
            -
                    return
         | 
| 120 | 
            -
                  end
         | 
| 121 | 
            -
                end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
                def validate_nonce(id_token, original_nonce, issuer, client_id)
         | 
| 125 | 
            -
                  jwks_uri = URI.parse("#{issuer}/.well-known/jwks.json")
         | 
| 126 | 
            -
                  jwks_response = Net::HTTP.get(jwks_uri)
         | 
| 127 | 
            -
                  jwks = JSON.parse(jwks_response)
         | 
| 128 | 
            -
                
         | 
| 129 | 
            -
                  decoded_token = JWT.decode(
         | 
| 130 | 
            -
                    id_token,
         | 
| 131 | 
            -
                    nil,
         | 
| 132 | 
            -
                    true,
         | 
| 133 | 
            -
                    algorithm: 'RS256',
         | 
| 134 | 
            -
                    iss: issuer,
         | 
| 135 | 
            -
                    aud: client_id,
         | 
| 136 | 
            -
                    verify_iss: true,
         | 
| 137 | 
            -
                    verify_aud: true,
         | 
| 138 | 
            -
                    jwks: { keys: jwks['keys'] }
         | 
| 139 | 
            -
                  )
         | 
| 140 | 
            -
                
         | 
| 141 | 
            -
                  payload = decoded_token[0]
         | 
| 142 | 
            -
                  nonce_from_token = payload['nonce']
         | 
| 143 | 
            -
                  
         | 
| 144 | 
            -
                  nonce_from_token == original_nonce
         | 
| 145 | 
            -
                rescue StandardError => e
         | 
| 146 | 
            -
                  Rails.logger.error("Nonce validation error: #{e.message}")
         | 
| 147 | 
            -
                  false
         | 
| 148 | 
            -
                end
         | 
| 149 | 
            -
                
         | 
| 150 | 
            -
                
         | 
| 151 | 
            -
              end
         | 
| 152 | 
            -
            end
         |