verikloak-audience 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 +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +19 -0
- data/README.md +85 -0
- data/lib/verikloak/audience/checker.rb +92 -0
- data/lib/verikloak/audience/configuration.rb +45 -0
- data/lib/verikloak/audience/errors.rb +34 -0
- data/lib/verikloak/audience/middleware.rb +63 -0
- data/lib/verikloak/audience/railtie.rb +49 -0
- data/lib/verikloak/audience/version.rb +9 -0
- data/lib/verikloak/audience.rb +38 -0
- data/lib/verikloak-audience.rb +8 -0
- metadata +96 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 983947348061c7b7dd379572850ec12de6bddda37b511b03466301b22c15256f
         | 
| 4 | 
            +
              data.tar.gz: 2bd99a2a76fdc020d4f3a51a1db11151d0b38e9429847294f0431abb7e47dd17
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 6bca90d90c7d35408009fcd6007ac317115743eda14dc4e8f1e9853a3fdb9350f4cb5db426e032631faf2e4ef9f96a86885942145bad40d7e809b5eb1a748905
         | 
| 7 | 
            +
              data.tar.gz: 3bcc1d7a44a094a3e15d8b33e79fe46ac66e21eb84175799d0469f701522949340e82b43df0d6e029a7396febc9c0ab16a196efdc1cce6bb25358b674019463e
         | 
    
        data/CHANGELOG.md
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # Changelog
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            All notable changes to this project will be documented in this file.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
         | 
| 6 | 
            +
            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            ---
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            ## [0.1.0] - 2025-09-20
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ### Added
         | 
| 13 | 
            +
            - Rack middleware for audience validation with configurable profiles (`:strict_single`, `:allow_account`, `:resource_or_aud`).
         | 
| 14 | 
            +
            - Configuration helpers and profile checker with suggestion logging.
         | 
| 15 | 
            +
            - Rails railtie for automatic middleware insertion after core Verikloak.
         | 
| 16 | 
            +
            - Error reference documentation (`ERRORS.md`) describing 403 responses and exception classes.
         | 
| 17 | 
            +
            - English README with installation, configuration tables, and integration guidance.
         | 
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            MIT License
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2025
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to do so, subject to the following
         | 
| 10 | 
            +
            conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in all
         | 
| 13 | 
            +
            copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
         | 
| 18 | 
            +
            COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
         | 
| 19 | 
            +
            ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,85 @@ | |
| 1 | 
            +
            # verikloak-audience
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Rack middleware for validating the `aud` claim of Keycloak-issued tokens on top of the Verikloak stack. It ships with deploy-friendly presets that address common Keycloak patterns such as `account` co-existence and `resource_access`-driven role enforcement.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            For the full error behaviour (response shapes, exception classes, logging hints), see [ERRORS.md](ERRORS.md).
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            > Insert the middleware immediately **after** `Verikloak::Middleware`. Doing so ensures the token is already verified and the claims are available via `env["verikloak.user"]` (default).
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## Why
         | 
| 10 | 
            +
            - Keycloak often emits multiple audiences (e.g. `["rails-api","account"]`)
         | 
| 11 | 
            +
            - Some deployments primarily rely on `resource_access[client].roles`
         | 
| 12 | 
            +
            - Re-implementing permissive/strict `aud` checks per app is a maintenance burden
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            ## Profiles
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            | Profile | Summary | Suggested scenarios / when `suggest_in_logs` points here |
         | 
| 17 | 
            +
            |---------|---------|-----------------------------------------------------------|
         | 
| 18 | 
            +
            | `:strict_single` *(recommended)* | Requires `aud` to match `required_aud` exactly (order-insensitive, no extras). | APIs where audiences are cleanly separated. Logged suggestion when the observed `aud` already equals the configured list. |
         | 
| 19 | 
            +
            | `:allow_account` | Allows `account` in addition to required audiences (e.g. `["rails-api","account"]`). | SPA + API mixes where Keycloak always emits `account`. Suggested when strict mode fails and the log shows `profile=:allow_account`. |
         | 
| 20 | 
            +
            | `:resource_or_aud` | Passes when `resource_access[client].roles` is present; otherwise falls back to `:allow_account`. | Services relying on resource roles. Suggested when logs output `profile=:resource_or_aud`. |
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            ## Installation
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            ```bash
         | 
| 25 | 
            +
            bundle add verikloak-audience
         | 
| 26 | 
            +
            ```
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            ## Rack / Rails usage
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            Insert **after** `Verikloak::Middleware`:
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            ```ruby
         | 
| 33 | 
            +
            # config/application.rb
         | 
| 34 | 
            +
            config.middleware.insert_after Verikloak::Middleware, Verikloak::Audience::Middleware,
         | 
| 35 | 
            +
              profile: :allow_account,
         | 
| 36 | 
            +
              required_aud: ["rails-api"],
         | 
| 37 | 
            +
              resource_client: "rails-api",
         | 
| 38 | 
            +
              env_claims_key: "verikloak.user",
         | 
| 39 | 
            +
              suggest_in_logs: true
         | 
| 40 | 
            +
            ```
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            See [`examples/rack.ru`](examples/rack.ru) for a full Rack sample. In Rails, always insert immediately after the core middleware; otherwise `env['verikloak.user']` will be empty and every request will fail with 403.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ## Configuration
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            | Option | Type | Default | Description |
         | 
| 47 | 
            +
            |--------|------|---------|-------------|
         | 
| 48 | 
            +
            | `profile` | Symbol | `:strict_single` | Profile selector. Accepts `:strict_single`, `:allow_account`, or `:resource_or_aud`. |
         | 
| 49 | 
            +
            | `required_aud` | Array/String/Symbol | `[]` | Required audience values; coerced to an array internally. |
         | 
| 50 | 
            +
            | `resource_client` | String | `"rails-api"` | Keycloak client id used to look up `resource_access[client].roles`. |
         | 
| 51 | 
            +
            | `env_claims_key` | String | `"verikloak.user"` | Rack env key where verified claims are stored. |
         | 
| 52 | 
            +
            | `suggest_in_logs` | Boolean | `true` | Emits a WARN log with the suggested profile when validation fails. |
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            `env_claims_key` assumes the preceding `Verikloak::Middleware` populates the Rack env. If the middleware order changes, claims will be missing and the audience check will always reject.
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            ## Testing
         | 
| 57 | 
            +
            All pull requests and pushes are automatically tested with [RSpec](https://rspec.info/) and [RuboCop](https://rubocop.org/) via GitHub Actions.
         | 
| 58 | 
            +
            See the CI badge at the top for current build status.
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            To run the test suite locally:
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            ```bash
         | 
| 63 | 
            +
            docker compose run --rm dev rspec
         | 
| 64 | 
            +
            docker compose run --rm dev rubocop -a
         | 
| 65 | 
            +
            ```
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            ## Contributing
         | 
| 68 | 
            +
            Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
         | 
| 69 | 
            +
             | 
| 70 | 
            +
            ## Security
         | 
| 71 | 
            +
            If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            ## License
         | 
| 74 | 
            +
            This project is licensed under the [MIT License](LICENSE).
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            ## Publishing (for maintainers)
         | 
| 77 | 
            +
            Gem release instructions are documented separately in [MAINTAINERS.md](MAINTAINERS.md).
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            ## Changelog
         | 
| 80 | 
            +
            See [CHANGELOG.md](CHANGELOG.md) for release history.
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            ## References
         | 
| 83 | 
            +
            - Verikloak (core): https://github.com/taiyaky/verikloak
         | 
| 84 | 
            +
            - verikloak-rails (Rails integration): https://github.com/taiyaky/verikloak-rails
         | 
| 85 | 
            +
            - verikloak-audience on RubyGems: https://rubygems.org/gems/verikloak-audience
         | 
| @@ -0,0 +1,92 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Verikloak
         | 
| 4 | 
            +
              module Audience
         | 
| 5 | 
            +
                # Audience profile checker functions.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                # This module provides predicate helpers used by the middleware to decide
         | 
| 8 | 
            +
                # whether a given set of claims satisfies the configured profile.
         | 
| 9 | 
            +
                module Checker
         | 
| 10 | 
            +
                  module_function
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  # Returns whether the given claims satisfy the configured profile.
         | 
| 13 | 
            +
                  #
         | 
| 14 | 
            +
                  # @param claims [Hash] OIDC claims (expects keys like "aud", "resource_access")
         | 
| 15 | 
            +
                  # @param cfg [Verikloak::Audience::Configuration]
         | 
| 16 | 
            +
                  # @return [Boolean]
         | 
| 17 | 
            +
                  def ok?(claims, cfg)
         | 
| 18 | 
            +
                    profile = cfg.profile.to_sym
         | 
| 19 | 
            +
                    profile = :strict_single unless %i[strict_single allow_account resource_or_aud].include?(profile)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    case profile
         | 
| 22 | 
            +
                    when :strict_single
         | 
| 23 | 
            +
                      strict_single?(claims, cfg.required_aud_list)
         | 
| 24 | 
            +
                    when :allow_account
         | 
| 25 | 
            +
                      allow_account?(claims, cfg.required_aud_list)
         | 
| 26 | 
            +
                    when :resource_or_aud
         | 
| 27 | 
            +
                      resource_or_aud?(claims, cfg.resource_client.to_s, cfg.required_aud_list)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # Validate that aud matches required exactly (order-insensitive).
         | 
| 32 | 
            +
                  #
         | 
| 33 | 
            +
                  # @param claims [Hash]
         | 
| 34 | 
            +
                  # @param required [Array<String>]
         | 
| 35 | 
            +
                  # @return [Boolean]
         | 
| 36 | 
            +
                  def strict_single?(claims, required)
         | 
| 37 | 
            +
                    aud = Array(claims['aud']).map(&:to_s)
         | 
| 38 | 
            +
                    return false if required.empty?
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    # Must contain all required and have no unexpected extra (order-insensitive)
         | 
| 41 | 
            +
                    (aud.sort == required.map(&:to_s).sort)
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # Validate aud allowing "account" as an extra value.
         | 
| 45 | 
            +
                  #
         | 
| 46 | 
            +
                  # @param claims [Hash]
         | 
| 47 | 
            +
                  # @param required [Array<String>]
         | 
| 48 | 
            +
                  # @return [Boolean]
         | 
| 49 | 
            +
                  def allow_account?(claims, required)
         | 
| 50 | 
            +
                    aud = Array(claims['aud']).map(&:to_s)
         | 
| 51 | 
            +
                    return false if required.empty?
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    # Permit 'account' extra
         | 
| 54 | 
            +
                    extras = aud - required
         | 
| 55 | 
            +
                    extras.delete('account')
         | 
| 56 | 
            +
                    extras.empty? && (required - aud).empty?
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # Permit when resource roles exist for the client; otherwise fallback to
         | 
| 60 | 
            +
                  # {#allow_account?}.
         | 
| 61 | 
            +
                  #
         | 
| 62 | 
            +
                  # @param claims [Hash]
         | 
| 63 | 
            +
                  # @param client [String]
         | 
| 64 | 
            +
                  # @param required [Array<String>]
         | 
| 65 | 
            +
                  # @return [Boolean]
         | 
| 66 | 
            +
                  def resource_or_aud?(claims, client, required)
         | 
| 67 | 
            +
                    roles = Array(claims.dig('resource_access', client, 'roles'))
         | 
| 68 | 
            +
                    return true unless roles.empty? # if roles for client exist, pass
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    # otherwise enforce allow_account semantics by default
         | 
| 71 | 
            +
                    allow_account?(claims, required)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  # Suggest which profile might fit better, for migration aid.
         | 
| 75 | 
            +
                  #
         | 
| 76 | 
            +
                  # @param claims [Hash]
         | 
| 77 | 
            +
                  # @param cfg [Verikloak::Audience::Configuration]
         | 
| 78 | 
            +
                  # @return [:strict_single, :allow_account, :resource_or_aud]
         | 
| 79 | 
            +
                  def suggest(claims, cfg)
         | 
| 80 | 
            +
                    aud = Array(claims['aud']).map(&:to_s)
         | 
| 81 | 
            +
                    req = cfg.required_aud_list
         | 
| 82 | 
            +
                    has_roles = !Array(claims.dig('resource_access', cfg.resource_client.to_s, 'roles')).empty?
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    return :strict_single if aud.sort == req.sort
         | 
| 85 | 
            +
                    return :allow_account if (aud - req) == ['account'] && (req - aud).empty?
         | 
| 86 | 
            +
                    return :resource_or_aud if has_roles
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    :strict_single
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Verikloak
         | 
| 4 | 
            +
              module Audience
         | 
| 5 | 
            +
                # Configuration holder for verikloak-audience.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                # @!attribute [rw] profile
         | 
| 8 | 
            +
                #   The enforcement profile to use.
         | 
| 9 | 
            +
                #   @return [:strict_single, :allow_account, :resource_or_aud]
         | 
| 10 | 
            +
                # @!attribute [rw] required_aud
         | 
| 11 | 
            +
                #   Required audience(s). Can be a String/Symbol or an Array of them.
         | 
| 12 | 
            +
                #   @return [Array<String,Symbol>, String, Symbol]
         | 
| 13 | 
            +
                # @!attribute [rw] resource_client
         | 
| 14 | 
            +
                #   Client id to use for `resource_access[client].roles` lookup.
         | 
| 15 | 
            +
                #   @return [String]
         | 
| 16 | 
            +
                # @!attribute [rw] env_claims_key
         | 
| 17 | 
            +
                #   Rack env key from which to read verified claims.
         | 
| 18 | 
            +
                #   @return [String]
         | 
| 19 | 
            +
                # @!attribute [rw] suggest_in_logs
         | 
| 20 | 
            +
                #   Whether to log a suggestion when audience validation fails.
         | 
| 21 | 
            +
                #   @return [Boolean]
         | 
| 22 | 
            +
                class Configuration
         | 
| 23 | 
            +
                  attr_accessor :profile, :required_aud, :resource_client,
         | 
| 24 | 
            +
                                :env_claims_key, :suggest_in_logs
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # Create a configuration with safe defaults.
         | 
| 27 | 
            +
                  #
         | 
| 28 | 
            +
                  # @return [void]
         | 
| 29 | 
            +
                  def initialize
         | 
| 30 | 
            +
                    @profile         = :strict_single
         | 
| 31 | 
            +
                    @required_aud    = []
         | 
| 32 | 
            +
                    @resource_client = 'rails-api'
         | 
| 33 | 
            +
                    @env_claims_key  = 'verikloak.user'
         | 
| 34 | 
            +
                    @suggest_in_logs = true
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Coerce `required_aud` into an array of strings.
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # @return [Array<String>]
         | 
| 40 | 
            +
                  def required_aud_list
         | 
| 41 | 
            +
                    Array(required_aud).map(&:to_s)
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Verikloak
         | 
| 4 | 
            +
              module Audience
         | 
| 5 | 
            +
                # Base error for audience failures.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                # @!attribute [r] code
         | 
| 8 | 
            +
                #   Machine-friendly error code (e.g. "insufficient_audience").
         | 
| 9 | 
            +
                #   @return [String]
         | 
| 10 | 
            +
                # @!attribute [r] http_status
         | 
| 11 | 
            +
                #   HTTP status code associated with the error.
         | 
| 12 | 
            +
                #   @return [Integer]
         | 
| 13 | 
            +
                class Error < StandardError
         | 
| 14 | 
            +
                  attr_reader :code, :http_status
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # @param msg [String] human-readable error message
         | 
| 17 | 
            +
                  # @param code [String] machine-friendly error code
         | 
| 18 | 
            +
                  # @param http_status [Integer] associated HTTP status
         | 
| 19 | 
            +
                  def initialize(msg = 'audience error', code: 'audience_error', http_status: 403)
         | 
| 20 | 
            +
                    super(msg)
         | 
| 21 | 
            +
                    @code = code
         | 
| 22 | 
            +
                    @http_status = http_status
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # Raised when audience is insufficient for the configured profile.
         | 
| 27 | 
            +
                class Forbidden < Error
         | 
| 28 | 
            +
                  # @param msg [String]
         | 
| 29 | 
            +
                  def initialize(msg = 'insufficient audience')
         | 
| 30 | 
            +
                    super(msg, code: 'insufficient_audience', http_status: 403)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'json'
         | 
| 4 | 
            +
            require 'verikloak/audience'
         | 
| 5 | 
            +
            require 'verikloak/audience/configuration'
         | 
| 6 | 
            +
            require 'verikloak/audience/checker'
         | 
| 7 | 
            +
            require 'verikloak/audience/errors'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Verikloak
         | 
| 10 | 
            +
              module Audience
         | 
| 11 | 
            +
                # Rack middleware that validates audience claims according to configured profile.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # Place this middleware after the core {::Verikloak::Middleware} so that
         | 
| 14 | 
            +
                # verified token claims are already available in the Rack env.
         | 
| 15 | 
            +
                class Middleware
         | 
| 16 | 
            +
                  # @param app [#call] next Rack application
         | 
| 17 | 
            +
                  # @param opts [Hash] configuration overrides (see {Configuration})
         | 
| 18 | 
            +
                  # @option opts [Symbol] :profile
         | 
| 19 | 
            +
                  # @option opts [Array<String>,String,Symbol] :required_aud
         | 
| 20 | 
            +
                  # @option opts [String] :resource_client
         | 
| 21 | 
            +
                  # @option opts [String] :env_claims_key
         | 
| 22 | 
            +
                  # @option opts [Boolean] :suggest_in_logs
         | 
| 23 | 
            +
                  def initialize(app, **opts)
         | 
| 24 | 
            +
                    @app = app
         | 
| 25 | 
            +
                    @config = Verikloak::Audience.configure
         | 
| 26 | 
            +
                    apply_overrides!(opts)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Evaluate the request against the audience profile.
         | 
| 30 | 
            +
                  #
         | 
| 31 | 
            +
                  # @param env [Hash] Rack environment
         | 
| 32 | 
            +
                  # @return [Array(Integer, Hash, #each)] Rack response triple
         | 
| 33 | 
            +
                  def call(env)
         | 
| 34 | 
            +
                    claims = env[@config.env_claims_key] || {}
         | 
| 35 | 
            +
                    return @app.call(env) if Checker.ok?(claims, @config)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    if @config.suggest_in_logs
         | 
| 38 | 
            +
                      suggestion = Checker.suggest(claims, @config)
         | 
| 39 | 
            +
                      aud_view = Array(claims['aud']).inspect
         | 
| 40 | 
            +
                      warn("[verikloak-audience] insufficient_audience; suggestion profile=:#{suggestion} aud=#{aud_view}")
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    body = { error: 'insufficient_audience',
         | 
| 44 | 
            +
                             message: "Audience not acceptable for profile #{@config.profile}" }.to_json
         | 
| 45 | 
            +
                    headers = { 'Content-Type' => 'application/json' }
         | 
| 46 | 
            +
                    [403, headers, [body]]
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  private
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  # Apply provided options to the configuration instance.
         | 
| 52 | 
            +
                  #
         | 
| 53 | 
            +
                  # @param opts [Hash]
         | 
| 54 | 
            +
                  # @return [void]
         | 
| 55 | 
            +
                  def apply_overrides!(opts)
         | 
| 56 | 
            +
                    cfg = @config
         | 
| 57 | 
            +
                    opts.each do |k, v|
         | 
| 58 | 
            +
                      cfg.public_send("#{k}=", v) if cfg.respond_to?("#{k}=")
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'rails/railtie'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Verikloak
         | 
| 6 | 
            +
              module Audience
         | 
| 7 | 
            +
                # Rails integration for verikloak-audience.
         | 
| 8 | 
            +
                #
         | 
| 9 | 
            +
                # This Railtie automatically inserts {Verikloak::Audience::Middleware}
         | 
| 10 | 
            +
                # into the Rails middleware stack directly after {::Verikloak::Middleware}
         | 
| 11 | 
            +
                # when it is present.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # If the core Verikloak middleware is not available, nothing is inserted.
         | 
| 14 | 
            +
                # You may still customize the placement in your application via
         | 
| 15 | 
            +
                # `config.middleware`.
         | 
| 16 | 
            +
                #
         | 
| 17 | 
            +
                # @example Configure placement and options
         | 
| 18 | 
            +
                #   # config/application.rb
         | 
| 19 | 
            +
                #   config.middleware.insert_after Verikloak::Middleware,
         | 
| 20 | 
            +
                #     Verikloak::Audience::Middleware,
         | 
| 21 | 
            +
                #     profile: :allow_account,
         | 
| 22 | 
            +
                #     required_aud: ['rails-api'],
         | 
| 23 | 
            +
                #     resource_client: 'rails-api',
         | 
| 24 | 
            +
                #     env_claims_key: 'verikloak.user',
         | 
| 25 | 
            +
                #     suggest_in_logs: true
         | 
| 26 | 
            +
                class Railtie < ::Rails::Railtie
         | 
| 27 | 
            +
                  # Adds the audience middleware after the core Verikloak middleware
         | 
| 28 | 
            +
                  # when available.
         | 
| 29 | 
            +
                  #
         | 
| 30 | 
            +
                  # @param app [Rails::Application] the Rails application instance
         | 
| 31 | 
            +
                  initializer 'verikloak_audience.middleware' do |app|
         | 
| 32 | 
            +
                    # Insert automatically after core verikloak if present
         | 
| 33 | 
            +
                    self.class.insert_middleware(app)
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # Performs the insertion into the middleware stack when the core
         | 
| 37 | 
            +
                  # Verikloak middleware is available. Extracted for testability without
         | 
| 38 | 
            +
                  # requiring a full Rails boot process.
         | 
| 39 | 
            +
                  #
         | 
| 40 | 
            +
                  # @param app [#middleware] An object exposing a Rack middleware stack via `#middleware`.
         | 
| 41 | 
            +
                  # @return [void]
         | 
| 42 | 
            +
                  def self.insert_middleware(app)
         | 
| 43 | 
            +
                    return unless defined?(::Verikloak::Middleware)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    app.middleware.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Verikloak::Audience provides Audience integration over Keycloak claims.
         | 
| 4 | 
            +
            require 'verikloak/audience/version'
         | 
| 5 | 
            +
            require 'verikloak/audience/configuration'
         | 
| 6 | 
            +
            require 'verikloak/audience/errors'
         | 
| 7 | 
            +
            require 'verikloak/audience/checker'
         | 
| 8 | 
            +
            require 'verikloak/audience/middleware'
         | 
| 9 | 
            +
            require 'verikloak/audience/railtie' if defined?(Rails::Railtie)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            module Verikloak
         | 
| 12 | 
            +
              # Audience configuration entrypoint and helpers.
         | 
| 13 | 
            +
              # This file also requires the public components of the gem.
         | 
| 14 | 
            +
              module Audience
         | 
| 15 | 
            +
                class << self
         | 
| 16 | 
            +
                  # Configure verikloak-audience.
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  # When a block is given, the current configuration is yielded so callers
         | 
| 19 | 
            +
                  # can mutate settings in one place.
         | 
| 20 | 
            +
                  #
         | 
| 21 | 
            +
                  # @yield [config] yields the current configuration for mutation
         | 
| 22 | 
            +
                  # @yieldparam config [Verikloak::Audience::Configuration]
         | 
| 23 | 
            +
                  # @return [Verikloak::Audience::Configuration] the resulting configuration
         | 
| 24 | 
            +
                  def configure
         | 
| 25 | 
            +
                    @config ||= Configuration.new
         | 
| 26 | 
            +
                    yield @config if block_given?
         | 
| 27 | 
            +
                    @config
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  # Access the current configuration without mutating it.
         | 
| 31 | 
            +
                  #
         | 
| 32 | 
            +
                  # @return [Verikloak::Audience::Configuration]
         | 
| 33 | 
            +
                  def config
         | 
| 34 | 
            +
                    @config ||= Configuration.new
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,8 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Minimal shim to load the namespaced entrypoint.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # This file preserves compatibility with Bundler's default require
         | 
| 6 | 
            +
            # (`require 'verikloak-audience'`) by delegating to the real entrypoint
         | 
| 7 | 
            +
            # under the namespaced path (`verikloak/audience`).
         | 
| 8 | 
            +
            require 'verikloak/audience'
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: verikloak-audience
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - taiyaky
         | 
| 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: rack
         | 
| 14 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 15 | 
            +
                requirements:
         | 
| 16 | 
            +
                - - ">="
         | 
| 17 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 18 | 
            +
                    version: '2.2'
         | 
| 19 | 
            +
                - - "<"
         | 
| 20 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 21 | 
            +
                    version: '4.0'
         | 
| 22 | 
            +
              type: :runtime
         | 
| 23 | 
            +
              prerelease: false
         | 
| 24 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 25 | 
            +
                requirements:
         | 
| 26 | 
            +
                - - ">="
         | 
| 27 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 28 | 
            +
                    version: '2.2'
         | 
| 29 | 
            +
                - - "<"
         | 
| 30 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 31 | 
            +
                    version: '4.0'
         | 
| 32 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 33 | 
            +
              name: verikloak
         | 
| 34 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 35 | 
            +
                requirements:
         | 
| 36 | 
            +
                - - ">="
         | 
| 37 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 38 | 
            +
                    version: 0.1.2
         | 
| 39 | 
            +
                - - "<"
         | 
| 40 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 41 | 
            +
                    version: '0.2'
         | 
| 42 | 
            +
              type: :runtime
         | 
| 43 | 
            +
              prerelease: false
         | 
| 44 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 45 | 
            +
                requirements:
         | 
| 46 | 
            +
                - - ">="
         | 
| 47 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 48 | 
            +
                    version: 0.1.2
         | 
| 49 | 
            +
                - - "<"
         | 
| 50 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 51 | 
            +
                    version: '0.2'
         | 
| 52 | 
            +
            description: |
         | 
| 53 | 
            +
              Rack middleware that enforces audience checks with deployable profiles,
         | 
| 54 | 
            +
              layering on top of Verikloak token verification.
         | 
| 55 | 
            +
            executables: []
         | 
| 56 | 
            +
            extensions: []
         | 
| 57 | 
            +
            extra_rdoc_files: []
         | 
| 58 | 
            +
            files:
         | 
| 59 | 
            +
            - CHANGELOG.md
         | 
| 60 | 
            +
            - LICENSE
         | 
| 61 | 
            +
            - README.md
         | 
| 62 | 
            +
            - lib/verikloak-audience.rb
         | 
| 63 | 
            +
            - lib/verikloak/audience.rb
         | 
| 64 | 
            +
            - lib/verikloak/audience/checker.rb
         | 
| 65 | 
            +
            - lib/verikloak/audience/configuration.rb
         | 
| 66 | 
            +
            - lib/verikloak/audience/errors.rb
         | 
| 67 | 
            +
            - lib/verikloak/audience/middleware.rb
         | 
| 68 | 
            +
            - lib/verikloak/audience/railtie.rb
         | 
| 69 | 
            +
            - lib/verikloak/audience/version.rb
         | 
| 70 | 
            +
            homepage: https://github.com/taiyaky/verikloak-audience
         | 
| 71 | 
            +
            licenses:
         | 
| 72 | 
            +
            - MIT
         | 
| 73 | 
            +
            metadata:
         | 
| 74 | 
            +
              source_code_uri: https://github.com/taiyaky/verikloak-audience
         | 
| 75 | 
            +
              changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
         | 
| 76 | 
            +
              bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
         | 
| 77 | 
            +
              documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.1.0
         | 
| 78 | 
            +
              rubygems_mfa_required: 'true'
         | 
| 79 | 
            +
            rdoc_options: []
         | 
| 80 | 
            +
            require_paths:
         | 
| 81 | 
            +
            - lib
         | 
| 82 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 83 | 
            +
              requirements:
         | 
| 84 | 
            +
              - - ">="
         | 
| 85 | 
            +
                - !ruby/object:Gem::Version
         | 
| 86 | 
            +
                  version: '3.1'
         | 
| 87 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 88 | 
            +
              requirements:
         | 
| 89 | 
            +
              - - ">="
         | 
| 90 | 
            +
                - !ruby/object:Gem::Version
         | 
| 91 | 
            +
                  version: '0'
         | 
| 92 | 
            +
            requirements: []
         | 
| 93 | 
            +
            rubygems_version: 3.6.9
         | 
| 94 | 
            +
            specification_version: 4
         | 
| 95 | 
            +
            summary: Audience profiles for Keycloak on top of Verikloak
         | 
| 96 | 
            +
            test_files: []
         |